SBE Domain Mapper Samples
In sbe-domain-mapping-samples-codegen.jar, you will find a collection of samples that use
the SBE Domain Mapper to generate proxies and adapters.
Each sample attempts to show the potential different uses of the various annotations.
Here is a brief list of the samples and what each tries to do:
-
BasicSubscriber: A simple subscriber that prints the contents of domain events it receives.
-
BasicPublisher: A simple publisher that sends domain events with a one-second pause between them.
-
BasicRingBuffer: A simple example that shows a publisher writing and subscriber reading from a ring buffer.
Each of the samples can be run with a simple script that can be found in:
sbe-domain-mapping-samples-distribution/bin/
The following will walk us through a basic sample of using SBE Domain Mapper. Please read the project sources and javadocs for detailed instructions as this article is focused on briefly explaining samples.
Domain Model
First let’s define our domain Model. We’ll visit class members and member functions as we look at codegen.
public class NewOrder
{
private final AsciiSequenceView clOrdID = new AsciiSequenceView(clOrdBuffer, 0, 20);
private final Instrument instrument;
private final MySide side;
private final Instant transactTime;
private final OrderQty orderQty;
private final MyOrdType orderType;
private final BigDecimal price;
private final String text;
}
Let’s now define our Domain Handlers.
NewOrderEventHandler will be used to proxy/adapt NewOrder domain event.
public interface NewOrderEventHandler
{
boolean event(NewOrder event);
}
NewOrderParameterHandler will be used to proxy/adapt parameters. We use the fields of our domain event so the example is easy to understand.
public interface NewOrderParameterHandler
{
boolean parameters(
AsciiSequenceView clOrdID,
Instrument instrument,
MySide side,
Instant transactTime,
OrderQty orderQty,
@SbePath("ordType") MyOrdType orderType,
BigDecimal price,
String text
);
}
Codegen Setup
Let’s define codegen in a different package to keep it separate from our domain model.
dependencies {
compileOnly(project(":sbe-domain-mapping-annotations"))
annotationProcessor(project(":sbe-domain-mapping-processor"))
annotationProcessor(files("src/main/resources"))
implementation(project(":sbe-domain-mapping-samples-domain-model"))
}
Let’s define our SBE schema and look at our SBE message.
<sbe:message name="NewOrder" id="1">
<field name="clOrdID" id="11" type="string20" semanticType="String"/>
<field name="instrument" id="55" type="Instrument" semanticType="Instrument"/>
<field name="side" id="54" type="Side" semanticType="char"/>
<field name="transactTime" id="60" type="timestamp" semanticType="UTCTimestamp"/>
<field name="orderQty" id="38" type="IntQty32" semanticType="Qty"/>
<field name="ordType" id="40" type="OrdType" semanticType="char"/>
<field name="price" id="44" type="OptionalPrice" semanticType="Price"/>
<data name="text" id="58" type="varAsciiEncoding"/>
</sbe:message>
Let’s define a few helpers that will help us convert between our domain model and SBE later.
AsciiDataViewFactory will allow us to convert from/to AsciiSequenceView and ASCII encoded SBE fields/data.
public class AsciiDataViewFactory
{
@DataViewFactory(charset = "US-ASCII")
public static AsciiSequenceView createAsciiSequenceView()
{
return new AsciiSequenceView();
}
}
InstantConverter will allow us to convert from/to Instant and SBE uint32,int64, or uint64.
public class InstantConverter
{
@ValueConverter
public static long convert(final Instant value)
{
return value == null ? Long.MIN_VALUE : value.toEpochMilli();
}
@ValueConverter
public static Instant convert(final long value)
{
return value == Long.MIN_VALUE ? null : Instant.ofEpochMilli(value);
}
}
Codegen Proxy/Adapter for Domain Events
Proxies and Adapter
Let’s define our domain event proxy and adapter.
Proxy
@GenerateProxyBaseClass is used to generate a base proxy named 'NewOrderEventBaseProxy' implementing methods defined in 'NewOrderEventHandler.class'.
@UseElementsFromPackage allows us to make use of helpers previously defined.
@GenerateProxyBaseClass(
className = "NewOrderEventBaseProxy",
targetClasses = { NewOrderEventHandler.class },
proxyOfferStrategy = ProxyOfferStrategy.OFFER
)
@UseElementsFromPackage("io.aeron.sbe.domain_mapping.samples.helpers")
public class NewOrderEventProxy extends NewOrderEventBaseProxy
{
}
The generated proxy generates an abstract method offer that we must implement.
Please see the sources to further understand implementation.
This method is called to publish a buffer with an SBE encoded method. In our sample code, we show a basic Aeron implementation.
Adapter
@GenerateAdapterBaseClass is used to generate a base adapter named 'NewOrderEventBaseAdapter' handling methods defined in 'NewOrderEventHandler.class'.
@UseElementsFromPackage allows us to make use of helpers previously defined. In addition, we will define methods to help us construct domain events and use this annotation to enable them.
@GenerateAdapterBaseClass(
className = "NewOrderEventBaseAdapter",
handlerClasses = { NewOrderEventHandler.class },
returnType = ControlledFragmentHandler.Action.class,
checkFieldValuesAreInRange = true,
preventBufferOverruns = true
)
@UseElementsFromPackage("io.aeron.sbe.domain_mapping.samples")
@UseElementsFromPackage("io.aeron.sbe.domain_mapping.samples.model")
@UseElementsFromPackage("io.aeron.sbe.domain_mapping.samples.helpers")
public class NewOrderEventAdapter extends NewOrderEventBaseAdapter implements ControlledFragmentHandler
{
}
The generated adapter generates a pair of abstract methods defaultResult and mapBooleanToAdapterResult that we must implement.
Domain Model
Let’s annotate our domain model.
Let’s annotate NewOrderEventHandler#event with @SbeMessage, defining which SBE message this method will use. @SbePath(SbePath.SELF) allows us to map SBE fields/group/data directly to NewOrder members.
public interface NewOrderEventHandler
{
@SbeMessage("io.aeron.sbe.domain_mapping.samples.NewOrder")
boolean event(@SbePath(SbePath.SELF) NewOrder event);
}
Let’s look at NewOrder again.
public class NewOrder
{
private final UnsafeBuffer clOrdBuffer = new UnsafeBuffer(new byte[20]);
private final AsciiSequenceView clOrdID = new AsciiSequenceView(clOrdBuffer, 0, 20);
private final Instrument instrument;
private final MySide side;
private final Instant transactTime;
private final OrderQty orderQty;
private final MyOrdType orderType;
private final BigDecimal price;
private final String text;
}
Let’s first define a constructor for NewOrder. @DomainFactory makes the constructor available for the generated adapter class.
@DomainFactory("io.aeron.sbe.domain_mapping.samples.NewOrder")
public NewOrder(
final AsciiSequenceView clOrdID,
final Instrument instrument,
final MySide side,
final Instant transactTime,
final OrderQty orderQty,
@SbePath("ordType") final MyOrdType orderType,
final BigDecimal price,
final String text
)
Let’s go through each member to understand how generated code uses each member for both the generated proxy and adapter. Every member has a getter member function to access member variables that the generated proxy directly uses.
1. AsciiSequenceView clOrdID
Generated proxy and adapter uses AsciiDataViewFactory (defined above) to allow us to map domain AsciiSequenceView to/from SBE ASCII character array based on naming.
2. Instrument instrument
Generated proxy maps Instrument record directly to SBE composite based on naming. @DomainFactory makes the static factory method available for the generated adapter class.
@DomainFactory("io.aeron.sbe.domain_mapping.samples.Instrument")
public static Instrument createInstrument(final String symbol)
{
return new Instrument(symbol);
}
public record Instrument(String symbol)
{
}
<composite name="Instrument" semanticType="Instrument">
<ref name="symbol" type="Symbol"/>
</composite>
3. MySide side
Generated proxy and adapter maps MySide enum directly to SBE enum based on naming. @SbeEnumCaseName defines the mapping between domain enum cases from/to SBE enum cases.
public enum MySide
{
@SbeEnumCaseName("BUY") RENAMED_BUY,
@SbeEnumCaseName("SELL") RENAMED_SELL;
}
<enum name="Side" encodingType="char">
<validValue name="BUY">1</validValue>
<validValue name="SELL">2</validValue>
</enum>
The generated adapter generates abstract methods to define null and unknown case for SBE enum cases.
protected MySide mapSideNullVal()
{
return null;
}
protected MySide mapSideUnknown()
{
return null;
}
4. Instant transactTime
Generated proxy and adapter uses InstantConverter (defined above) to allow us to map domain Instant to/from SBE uint64.
5. OrderQty orderQty
Generated proxy maps OrderQty record directly to SBE composite based on naming. @DomainFactory makes the record constructor method available for the generated adapter class.
@DomainFactory("io.aeron.sbe.domain_mapping.samples.IntQty32")
public record OrderQty(int mantissa, byte exponent)
{
}
<composite name="IntQty32" semanticType="Qty">
<type name="mantissa" primitiveType="int32"/>
<type name="exponent" primitiveType="int8"/>
</composite>
6. MyOrdType orderType
Generated proxy and adapter maps MyOrdType enum directly to SBE enum. Cases are automatically mapped based on naming.
public enum MyOrdType
{
MARKET_ORDER,
LIMIT_ORDER,
STOP_ORDER,
STOP_LIMIT_ORDER,
MARKET_LIMIT_ORDER;
}
<enum name="OrdType" encodingType="char">
<validValue name="MARKET_ORDER">1</validValue>
<validValue name="LIMIT_ORDER">2</validValue>
<validValue name="STOP_ORDER">3</validValue>
<validValue name="STOP_LIMIT_ORDER">4</validValue>
<validValue name="MARKET_LIMIT_ORDER">K</validValue>
</enum>
Member orderType is not mapped to SBE field ordType based on naming. We use @SbePath in constructor and getter to allow generated proxy and adapter to map method and constructor parameter to appropriate SBE field.
public NewOrder(
...
@SbePath("ordType") final MyOrdType orderType
...
)
@SbePath("ordType")
public MyOrdType getOrderType()
{
return orderType;
}
The generated adapter generates abstract methods to define null and unknown case for SBE enum cases.
protected MyOrdType mapOrdTypeNullVal()
{
return null;
}
protected MyOrdType mapOrdTypeUnknown()
{
return null;
}
7. BigDecimal price
Generated proxy and adapter has no automatic or defined mapping domain BigDecimal to/from SBE composite.
<composite name="OptionalPrice">
<type name="mantissa" primitiveType="int64"/>
<type name="exponent" primitiveType="int8"/>
</composite>
The generated proxy and adapter generate abstract methods to define mapping that we must implement.
Proxy:
protected long mapEventGetPriceToPriceMantissa(final BigDecimal price)
{
return price.unscaledValue().longValue();
}
protected byte mapEventGetPriceToPriceExponent(final BigDecimal price)
{
return (byte)price.scale();
}
Adapter:
protected BigDecimal mapNewOrderPriceToPrice(final OptionalPriceDecoder price)
{
return BigDecimal.valueOf(price.mantissa(), price.exponent());
}
Using Proxy and Adapter
We can now use proxy to send domain events and adapter to receive domain events.
Proxy:
final UnsafeBuffer clOrdIdBuffer =
new UnsafeBuffer("clOrdID-123456789101".getBytes(StandardCharsets.US_ASCII));
final NewOrder newOrder = new NewOrder(
new AsciiSequenceView(clOrdIdBuffer, 0, 20),
new Instrument("SYMBOL"),
MySide.RENAMED_SELL,
Instant.now(),
new OrderQty(9999, (byte)2),
MyOrdType.LIMIT_ORDER,
new BigDecimal("99.99"),
"Please bid"
);
final boolean success = aeronProxy.event(newOrder);
Adapter:
final NewOrderEventHandler eventHandler = new NewOrderEventHandlerImpl();
final NewOrderEventAdapter eventAdapter = new NewOrderEventAdapter(eventHandler);
final ControlledFragmentAssembler fragmentAssembler = new ControlledFragmentAssembler(eventAdapter);
while (RUNNING.get())
{
final int fragments = subscription.controlledPoll(fragmentAssembler, 10);
IDLE_STRATEGY.idle(fragments);
}
Codegen Proxy/Adapter for Parameters
Proxies and Adapter
Let’s define our parameter-based proxy and adapter.
Proxy
@GenerateProxyBaseClass is used to generate a base proxy named 'NewOrderParameterBaseProxy' implementing methods defined in 'NewOrderParameterHandler.class'.
@UseElementsFromPackage allows us to make use of helpers previously defined.
@GenerateProxyBaseClass(
className = "NewOrderParameterBaseProxy",
targetClasses = { NewOrderParameterHandler.class },
proxyOfferStrategy = ProxyOfferStrategy.TRY_CLAIM
)
@UseElementsFromPackage("io.aeron.sbe.domain_mapping.samples.helpers")
public class NewOrderParameterProxy extends NewOrderParameterBaseProxy
{
}
The generated proxy generates abstract methods tryClaim, isTryClaimSuccessful, claimedBuffer, claimedOffset, commit that we must implement.
Please see the sources to further understand the implementation.
This method is called to claim a range within a buffer which an SBE message is directly encoded into. In our sample code, we show a basic Agrona RingBuffer implementation.
Adapter
@GenerateAdapterBaseClass is used to generate a base adapter named 'NewOrderParameterBaseAdapter' handling methods defined in 'NewOrderParameterHandler.class'.
@UseElementsFromPackage allows us to make use of helpers previously defined. In addition, we will define methods to help us construct domain events and use this annotation to enable them.
@GenerateAdapterBaseClass(
className = "NewOrderParameterBaseAdapter",
handlerClasses = { NewOrderParameterHandler.class },
returnType = ControlledMessageHandler.Action.class,
checkFieldValuesAreInRange = false,
preventBufferOverruns = false
)
@UseElementsFromPackage("io.aeron.sbe.domain_mapping.samples")
@UseElementsFromPackage("io.aeron.sbe.domain_mapping.samples.model")
@UseElementsFromPackage("io.aeron.sbe.domain_mapping.samples.helpers")
public class NewOrderParameterAdapter extends NewOrderParameterBaseAdapter implements ControlledMessageHandler
{
}
The generated adapter generates a pair of abstract methods defaultResult and mapBooleanToAdapterResult that we must implement.
Domain Model
Let’s annotate our domain model.
Let’s annotate NewOrderParameterHandler#event with @SbeMessage, defining which SBE message this method will use. The SBE fields/group/data map directly to the handler’s parameters.
public interface NewOrderParameterHandler
{
@SbeMessage("io.aeron.sbe.domain_mapping.samples.NewOrder")
boolean parameters(
AsciiSequenceView clOrdID,
Instrument instrument,
MySide side,
Instant transactTime,
OrderQty orderQty,
@SbePath("ordType") MyOrdType orderType,
BigDecimal price,
String text
);
}
Let’s go through each member to understand how generated code uses each member for both the generated proxy and adapter.
1. AsciiSequenceView clOrdID
Generated proxy and adapter uses AsciiDataViewFactory (defined above) to allow us to map domain AsciiSequenceView to/from SBE ASCII character array based on naming.
2. Instrument instrument
Generated proxy maps Instrument record directly to SBE composite based on naming. @DomainFactory makes the static factory method available for the generated adapter class.
@DomainFactory("io.aeron.sbe.domain_mapping.samples.Instrument")
public static Instrument createInstrument(final String symbol)
{
return new Instrument(symbol);
}
public record Instrument(String symbol)
{
}
<composite name="Instrument" semanticType="Instrument">
<ref name="symbol" type="Symbol"/>
</composite>
3. MySide side
Generated proxy and adapter maps MySide enum directly to SBE enum based on naming. @SbeEnumCaseName defines the mapping between domain enum cases from/to SBE enum cases.
public enum MySide
{
@SbeEnumCaseName("BUY") RENAMED_BUY,
@SbeEnumCaseName("SELL") RENAMED_SELL;
}
<enum name="Side" encodingType="char">
<validValue name="BUY">1</validValue>
<validValue name="SELL">2</validValue>
</enum>
The generated adapter generate abstract methods to define null and unknown case for SBE enum cases.
protected MySide mapSideNullVal()
{
return null;
}
protected MySide mapSideUnknown()
{
return null;
}
4. Instant transactTime
Generated proxy and adapter uses InstantConverter (defined above) to allow us to map domain Instant to/from SBE uint64.
5. OrderQty orderQty
Generated proxy maps OrderQty record directly to SBE composite based on naming. @DomainFactory makes the record constructor method available for the generated adapter class.
@DomainFactory("io.aeron.sbe.domain_mapping.samples.IntQty32")
public record OrderQty(int mantissa, byte exponent)
{
}
<composite name="IntQty32" semanticType="Qty">
<type name="mantissa" primitiveType="int32"/>
<type name="exponent" primitiveType="int8"/>
</composite>
6. MyOrdType orderType
Generated proxy and adapter maps MyOrdType enum directly to SBE enum. Cases are automatically mapped based on naming.
public enum MyOrdType
{
MARKET_ORDER,
LIMIT_ORDER,
STOP_ORDER,
STOP_LIMIT_ORDER,
MARKET_LIMIT_ORDER;
}
<enum name="OrdType" encodingType="char">
<validValue name="MARKET_ORDER">1</validValue>
<validValue name="LIMIT_ORDER">2</validValue>
<validValue name="STOP_ORDER">3</validValue>
<validValue name="STOP_LIMIT_ORDER">4</validValue>
<validValue name="MARKET_LIMIT_ORDER">K</validValue>
</enum>
Member orderType is not mapped to SBE field ordType based on naming. We use @SbePath in constructor and parameter-based handler method to allow generated proxy and adapter to map.
public NewOrder(
...
@SbePath("ordType") final MyOrdType orderType
...
)
boolean parameters(
...
@SbePath("ordType") MyOrdType orderType,
...
);
The generated adapter generates abstract methods to define null and unknown case for SBE enum cases.
protected MyOrdType mapOrdTypeNullVal()
{
return null;
}
protected MyOrdType mapOrdTypeUnknown()
{
return null;
}
7. BigDecimal price
Generated proxy and adapter has no automatic or defined mapping domain BigDecimal to/from SBE composite.
<composite name="OptionalPrice">
<type name="mantissa" primitiveType="int64"/>
<type name="exponent" primitiveType="int8"/>
</composite>
The generated proxy and adapter generate abstract methods to define mapping that we must implement.
Proxy:
protected long mapEventGetPriceToPriceMantissa(final BigDecimal price)
{
return price.unscaledValue().longValue();
}
protected byte mapEventGetPriceToPriceExponent(final BigDecimal price)
{
return (byte)price.scale();
}
Adapter:
protected BigDecimal mapNewOrderPriceToPrice(final OptionalPriceDecoder price)
{
return BigDecimal.valueOf(price.mantissa(), price.exponent());
}
Using Proxy and Adapter
We can now use proxy to send parameters and adapter to receive parameters.
Proxy:
final UnsafeBuffer clOrdIdBuffer = new UnsafeBuffer("clOrdID-123456789101".getBytes(StandardCharsets.US_ASCII));
final boolean result = parameterProxy.parameters(
new AsciiSequenceView(clOrdIdBuffer, 0, 20),
new Instrument("SYMBOL"),
MySide.RENAMED_BUY,
Instant.now(),
new OrderQty(10000, (byte)2),
MyOrdType.MARKET_ORDER,
new BigDecimal("100"),
"Please offer"
);
Adapter:
final NewOrderParameterHandler parameterHandler = new NewOrderParameterHandlerImpl();
this.parameterAdapter = new NewOrderParameterAdapter(parameterHandler);
ringBuffer.controlledRead(parameterAdapter, 1);