SBE Domain Mapper

The SBE Domain Mapper generates proxy and adapter classes that convert between SBE (Simple Binary Encoding) encoded messages and domain representations. This guide will walk you through setting up the tool in a Gradle project.


Prerequisites

  • Java 17 or later.

  • Access to the private Artifactory repository where the sbe-domain-mapping-processor artifact is hosted.

  • SBE schema files for the messages you want to send and receive.


Gradle Setup

1. Add the Private Artifactory Repository

SBE Domain Mapper is a Premium Aeron component. The binaries are accessible from the Adaptive Artifactory.

  • sbe-domain-mapping-processor - Main module.

    <groupId>io.aeron.premium.sbe.extensions</groupId>
    <artifactId>sbe-domain-mapping-processor</artifactId>
  • sbe-domain-mapping-annotations - Annotations.

    <groupId>io.aeron.premium.sbe.extensions</groupId>
    <artifactId>sbe-domain-mapping-annotations</artifactId>

2. Add SBE Schema Files

  1. Place your SBE schema files (XML) in a directory in your project, e.g., src/main/resources/sbe.

  2. Create a manifest file named sbe-schema-manifest.txt in the src/main/resources directory. This file should list the relative paths to your SBE schema files, one per line. For example:

    sbe/schema1.xml
    sbe/schema2.xml
  3. Ensure the src/main/resources directory (or the directory containing the manifest and schema files) is visible to the annotation processor by adding it to the annotationProcessor classpath in your build.gradle file:

    dependencies {
        compileOnly "io.aeron.premium.sbe.extensions:sbe-domain-mapping-annotations:$VERSION"
        annotationProcessor "io.aeron.premium.sbe.extensions:sbe-domain-mapping-processor:$VERSION"
        annotationProcessor files("src/main/resources")
    }

4. Add Compile Options

Adjust Java Compilation task to open java.base/jdk.internal.misc because Agrona makes use of an internal JDK module and the annotation processor uses Agrona for SBE IR.

tasks.withType(JavaCompile::class).configureEach {
    options.isFork = true
    options.forkOptions.jvmArgs = listOf("--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED")
}

Handling Schemas in a Separate Module

If the SBE schema files are defined in a separate Gradle module (e.g., :sbe-schemas), you can include the resources from that module in the annotationProcessor classpath like this:

dependencies {
    annotationProcessor "io.aeron.premium.sbe.extensions:sbe-domain-mapping-processor:$VERSION"
    annotationProcessor project(":sbe-schemas")
}

Ensure the :sbe-schemas module includes the schema files and manifest in its src/main/resources directory.


Key Annotations

@GenerateAdapterBaseClass

Triggers the generation of a base adapter class that decodes SBE messages, adapts representations, and calls appropriate event handler methods where handlers are listed in handlerClasses and the methods are annotated with @SbeMessage. The generated class serves as a bridge between SBE messages and domain logic.

The generated adapters can make use of @DomainFactory, @ValueConverter, and @DataViewFactory methods to adapt between SBE and domain representations. These are discussed in subsequent sections.

Example:

@GenerateAdapterBaseClass(
    className = "MyAdapterBase",
    handlerClasses = {MyHandler.class},
    returnType = void.class
)
public class MyAdapter extends MyAdapterBase
{
    // Custom implementation
}

@GenerateProxyBaseClass

Triggers the generation of a base proxy class that handles the encoding of SBE messages for each of the proxy methods where the proxy class is listed in targetClasses and the method is annotated with @SbeMessage. The generated class serves as a bridge between domain logic and SBE messages.

The generated proxies can make use of @ValueConverter and @DataViewFactory methods to adapt between domain and SBE representations. These are discussed in subsequent sections.

Example:

@GenerateProxyBaseClass(
    className = "MyProxyBase",
    targetClasses = {MyHandler.class},
    proxyOfferStrategy = ProxyOfferStrategy.OFFER
)
public class MyProxy extends MyProxyBase
{
    // Custom implementation
}

@SbeMessage

Marks a method in a handler class as capable of processing a specific SBE message. The annotated method can either accept the SBE fields directly as parameters or accept a domain event created via a @DomainFactory.

This annotation is also used for methods on proxy interfaces to indicate that calling the method will encode a particular SBE message.

Example:

public class Participants
{
    @SbeMessage("com.example.sbe.AddParticipantCommand")
    public void addParticipant(long participantId, long correlationId, String name)
    {
        // Handle SBE message fields directly
    }

    @SbeMessage("com.example.sbe.CreateParticipantEvent")
    public void onCreateParticipant(@SbePath(SbePath.SELF) ParticipantCreatedEvent event)
    {
        // Handle a domain event created via a @DomainFactory
    }
}

@DomainFactory

Marks a constructor, instance method, static method, or type (with a single constructor) as a conversion for a specific SBE type or message. The annotated executable’s parameters should match the fields of the SBE type or message, though adjustments can be made using @SbePath. Fields may be omitted if they are not needed in the domain.

The annotation processor will only use @DomainFactory methods, when generating adapters, that are accessible and made available by the @UseElementsFromPackage annotation (described in a subsequent section).

Example:

// Constructor mapping
@DomainFactory("com.example.sbe.ParticipantEvent")
public record ParticipantCreatedEvent(long participantId, String name)
{
    // Create a domain event from SBE fields
}

// Static factory method mapping
@DomainFactory("com.example.sbe.AuctionEvent")
public static AuctionCreatedEvent createAuction(long auctionId, String description)
{
    // Create a domain event from SBE fields
}

@ValueConverter

Makes an instance or static method available as a converter between types. For example, to convert backwards and forwards between a long timestamp value (an SBE representation) and a java.time.Instant object (a domain representation). A value converter must accept a single parameter and return a non-void type.

When generating adapter code that converts an SBE field/data value into a parameter of a domain handler or domain factory, the annotation processor will look for a matching converter method that accepts the "default Java SBE field representation type" and returns the domain’s parameter type.

When generating proxy code that converts the value an accessor returns into an SBE field or data value, the annotation processor will look for a matching converter method that accepts the domain accessor’s return type and returns the "default Java SBE field representation type".

The annotation processor will only use @ValueConverter methods, when generating proxies and adapters, that are accessible and made available by the @UseElementsFromPackage annotation (described in a subsequent section).

Example:

public class TimestampConverter
{
    @ValueConverter
    public static Instant toInstant(long timestamp)
    {
        return Instant.ofEpochMilli(timestamp);
    }

    @ValueConverter
    public static long toLong(Instant instant)
    {
        return instant.toEpochMilli();
    }
}

Default Java SBE field representation types:

  • byte for char

  • byte[] for an array of char with a null character set

  • String for an array of char with a non-null character set

  • byte for int8

  • byte[] for an array of int8

  • short for uint8

  • short[] for an array of uint8

  • short for int16

  • short[] for an array of int16

  • int for uint16

  • int[] for an array of uint16

  • int for int32

  • int[] for an array of int32

  • long for uint32

  • long[] for an array of uint32

  • long for int64

  • long[] for an array of int64

  • long for uint64

  • long[] for an array of uint64

  • float for float32

  • float[] for an array of float32

  • double for float64

  • double[] for an array of float64

  • byte[] for variable-length data with a null character set

  • String for variable-length data with a non-null character set


@DataViewFactory

Marks a constructor, instance method, static method, or type (with a single constructor) as a factory for a data view. A data view is typically a flyweight object that provides a view of some underlying SBE data. For example: - Agrona’s AsciiSequenceView is a view over an ascii sequence in a DirectBuffer, and - Agrona’s UnsafeBuffer can be a view over a sequence of bytes in a DirectBuffer.

Users can either use these Agrona classes or provide their own data view implementations. The data view factory method or constructor must have no parameters.

For use in generated adapters, the returned data view class must expose a void wrap(DirectBuffer buffer, int offset, int length) method. The annotation processor will generate an adapter that calls this method to wrap the data view around the SBE field or data section when the data view type matches the domain handler or factory’s parameter type and the matching SBE path is either an array or a variable-length data section.

For use in generated proxies, the returned data view class must either:

  1. be a subclass of DirectBuffer, where the underlying data is represented by all the bytes from 0 to capacity() (much like UnsafeBuffer), or

  2. expose DirectBuffer buffer(), int offset(), and int length() methods (much like AsciiSequenceView).

In cases where the data view is only applicable to data with a specific character set, a charset property can be specified.

The annotation processor will only process data views with @DataViewFactory methods, when generating proxies and adapters, that are accessible and made available by the @UseElementsFromPackage annotation (described in a subsequent section).

Example:

public class DataViewFactories
{
    @DataViewFactory
    public static UnsafeBuffer createByteSequenceView()
    {
        return new UnsafeBuffer();
    }

    @DataViewFactory(charset = "US-ASCII")
    public static AsciiSequenceView createAsciiSequenceView()
    {
        return new AsciiSequenceView();
    }
}

@SbePath

Specifies a dot-separated path to a field within an SBE message or composite, relative to the current mapping context of the @DomainFactory or @SbeMessage. This allows for navigating nested or composite structures within SBE messages. When a parameter is not annotated, the parameter name (if available) will be used as the SBE field path.

Example:

@DomainFactory("com.example.sbe.MyMessage")
public DomainEvent createEvent(
    @SbePath("someComposite.timestamp") long timestamp,
    @SbePath("anotherComposite.value") double value,
    @SbePath("someSet.myFlag") boolean myFlag)
{
    // Create a domain event using nested SBE fields
}

@SbePath(SbePath.SELF)

Specifies that the mapping context for a parameter is the same as the current mapping context of the @DomainFactory or @SbeMessage. This is useful for mapping SBE messages into domain events. It is also useful for modelling structures in the domain that aren’t directly represented in the SBE schema, e.g., due to constraints around message evolution where structure in the SBE message via composites could not change over time.

Example:

@DomainFactory("com.example.sbe.MyMessage")
public record Foo(
    int bar,
    @SbePath(SbePath.SELF) Baz baz
)
{
}

@DomainFactory("com.example.sbe.MyMessage")
public record Baz(
    @SbePath("bazFoo") int foo,
    @SbePath("bazBar") int bar
)
{
}
<sbe:message name="MyMessage" id="1">
    <field name="bar" id="1" type="int32"/>
    <field name="bazFoo" id="2" type="int32"/>
    <field name="bazBar" id="3" type="int32" sinceVersion="2"/>
</sbe:message>

@SbeEnumCaseName

Maps a specific SBE enum value to a case name, typically used for converting SBE enums into domain-specific enums or constants.

Example:

public enum DomainStatus
{
    @SbeEnumCaseName("A")
    ACTIVE,

    @SbeEnumCaseName("I")
    INACTIVE,

    @SbeEnumCaseName("P")
    PENDING
}

@UseElementsFromPackage

Specifies a package, from which elements annotated with @DomainFactory, @ValueConverter, and @DataViewFactory, that the annotation processor can use when generating code, e.g., to perform conversions. This annotation is used in conjunction with @GenerateAdapterBaseClass to control which mappings are available for the generated adapter.

Example:

@GenerateAdapterBaseClass(
    className = "MyAdapterBase",
    handlerClasses = {MyHandler.class},
    returnType = void.class
)
@UseElementsFromPackage("com.example.foo")
@UseElementsFromPackage("com.example.bar")
public class MyAdapter extends MyAdapterBase
{
    // Custom implementation
}

Parameter Mapping and Unmapped Parameters

Parameter Mapping within Generated Adapters

Generated adapters will associate SBE fields to parameters using the following rules, in order of precedence:

  1. @SbePath: If a parameter is annotated with @SbePath, the generator uses the specified path to locate the field in the SBE message.

  2. Parameter Name: If no @SbePath is provided, the generator attempts to match the parameter name to an SBE field name.

The parameter associated with an SBE field may or may not have a matching type. Generated adapters can convert between SBE and domain representations using the following approaches:

  • When the SBE type is a message or a composite:

    • The adapter can use a @DomainFactory to convert the complex SBE type into a domain object where:

    • The parameter type matches the type returned by the factory

    • When the SBE type is a block field or variable-length data section:

    • The adapter can use a @ValueConverter to convert the SBE type into a domain representation where:

    • The handler’s parameter type matches the type returned by the converter

    • The converter’s parameter type matches the default Java SBE field representation type

  • When the SBE type is an array field or variable-length data section:

    • The adapter can use a data view to represent the underlying data where:

      • The adapter can construct a reusable data view via a @DataViewFactory method

      • The data view type matches the handler’s parameter type

    • If annotated with a charset, the factory’s charset matches the SBE field’s charset

In cases where multiple conversions are applicable, i.e., the conversion is ambiguous, the annotation processor will generate an error. In cases where a conversion is possible and the default SBE Java representation type matches the handler’s parameter type, the generated adapter will use the conversion.

If the generator cannot map a parameter using these rules, it will generate an abstract method in the base adapter class for you to supply the missing argument manually.


Handling Unmapped Parameters and Repeating Groups

For parameters that are not automatically mapped (including repeating groups), the code generator creates an abstract method in both cases of the base adapter and base proxy classes.

For the adapter, this method allows you to supply the missing argument or handle repeating groups manually.

For example:

protected abstract SomeType supplyUnmappedParameter(SomeSbeMessageDecoder decoder);

protected abstract MyListType mapRepeatingGroup(SomeSbeMessageDecoder decoder);

For the proxy, this method allows you to encode repeating groups manually.

For example:

protected abstract SomeType supplyUnmappedParameter(SomeProxyMethodParamater parameter);

protected abstract void mapRepeatingGroup(SomeSbeMessageEncoder encoder, SomeProxyMethodParamater parameter);
Important Notes:
  • You are responsible for rewinding the SBE decoder and skipping fields if necessary to ensure correct field access order.

  • The decoder passed to the method may not be positioned at the start of the message.


Handling Return Types

If the handler methods return a different type to the adapter’s return type, the code generator will create an abstract method to map the handler’s result to the adapter’s return type.

Example: Mapping boolean to ControlledFragmentHandler.Action

A common use case is converting a boolean return type from the handler (e.g., to indicate success or backpressure) to Aeron’s ControlledFragmentHandler.Action. Here’s an example implementation:

@Override
protected ControlledFragmentHandler.Action mapHandlerResult(boolean handlerResult)
{
    return handlerResult ? ControlledFragmentHandler.Action.CONTINUE : ControlledFragmentHandler.Action.ABORT;
}

In this example:

  • If the handler returns true, the adapter returns ControlledFragmentHandler.Action.CONTINUE to indicate that processing should continue.

  • If the handler returns false, the adapter returns ControlledFragmentHandler.Action.ABORT to indicate that processing should stop (e.g., due to backpressure).


Parameter Mapping within Generated Proxies

Generated adapters will associate SBE fields to parameters using the following rules, in order of precedence:

  1. @SbePath: If a parameter or accessor method is annotated with @SbePath, the generator uses the specified path to locate the field in the SBE message.

  2. Parameter or Accessor Method Name: If no @SbePath is provided, the generator attempts to match the parameter name or accessor method name (without the get prefix) to an SBE field name.

The parameter or accessor associated with an SBE field may or may not have a matching type. Generated proxies can convert between SBE and domain representations using the following approaches:

  • When the SBE type is a block field or variable-length data section:

    • The proxy can use a @ValueConverter to convert the domain representation into an SBE representation where:

      • The proxy method’s parameter type (or domain event’s accessor) matches the type accepted by the converter

      • The converter’s return type matches the default Java SBE field representation type

  • When the SBE type is an array field or variable-length data section:

    • The proxy can use a data view to represent the data being supplied where:

      • The data view type matches the proxy method’s parameter (or domain event’s accessor) type

    • If annotated with a charset, the factory’s charset matches the SBE field’s charset

In cases where multiple conversions are applicable, i.e., the conversion is ambiguous, the annotation processor will generate an error. In cases where a conversion is possible and the default SBE Java representation type matches the handler’s parameter type, the generated proxy will use the conversion.

If the generator cannot map a parameter using these rules, it will generate an abstract method in the base proxy class for you to supply the missing SBE value manually.


Troubleshooting

Missing SBE Schemas

If the tool cannot find a message in the available SBE schemas, it will produce an error message during compilation. To resolve this:

  1. Ensure the sbe-schema-manifest.txt file exists and contains the correct paths to your SBE schema files.

  2. Verify that the schema files are in the specified location.

  3. Ensure the src/main/resources directory (or the directory containing the manifest and schema files) is included in the annotationProcessor classpath in your build.gradle file.


Support

For additional help, contact the Aeron support team.