SBE: Simple Binary Encoding

The binary message format of the FIX Trading Community, used where every microsecond counts: exchange order entry, market data feeds and journals. Prayog uses it for its journal. First the idea, then the bytes, then how Prayog uses it.

1. The problem

Every order Prayog receives is written to disk before anyone is told about it. That means turning a Java object into bytes (encoding) and, on replay, bytes back into an object (decoding), millions of times a day. The format needs four properties:

Text formats like JSON fail the first two. SBE was designed for exactly this list.

2. A business example: the pre-printed form

Imagine two ways a bank branch could send account-opening requests to head office.

The form has one rule that matters later: you can add new boxes at the bottom, but never move or reuse an old one. Old clerks ignore the new boxes and keep working.

3. How SBE works

  1. Write an XML schema listing messages, their fields and their types (int64, uint8, enums, strings).
  2. Generate code. sbe-tool turns the schema into Java classes, one encoder and one decoder per message, at build time.
  3. Encode in place. An encoder is a flyweight: a tiny reusable object that "wraps" a buffer at an offset and writes each field straight to its fixed position. No intermediate object, no garbage.
  4. Decode in place. A decoder wraps the bytes and reads a field only when asked, by jumping to its offset. Reading the price of an order never touches the symbol.

The layout of a message

Header (8 bytes)length, id, schema, version Fixed blockevery field at a known offset Repeating groupscount, then entries Variable datalength + bytes, in order Fixed parts first, so their offsets never depend on how long a string is.
Byte order: Prayog's schema is little-endian, as SBE recommends. x86 and ARM are both little-endian, so encoding is a plain memory store with no byte swapping.

4. Try it: encode a NewOrder

This reproduces Prayog's real NewOrder layout from journal-schema.xml. Change a field and watch which bytes move. Notice that a longer client order id never shifts the price.

headeraccountIdside, type pricequantitystrings (length + bytes)

With the default values, the bytes above are exactly the ones pinned by JournalCodecTest.layoutIsPinned.

5. Changing a schema safely

A journal written today must replay next year. SBE supports that with a few rules:

ChangeSafe?Why
Add a field at the end of the fixed block, bump the versionYesOld decoders use blockLength from the header to skip what they don't know. New decoders see an old version and return a null value for the new field (sinceVersion).
Add a new message typeYesNew template id; old code never sees it in old journals.
Reorder or remove a fieldNoEvery later offset moves. Old bytes decode into the wrong fields.
Renumber an enum valueNoOld journals hold the old number. "SELL" would read as something else.
Reuse a template idNoOld records would decode as the new message.

This is why Prayog's schema numbers its enum values explicitly instead of trusting the Java enum's order. If someone reorders RejectReason in Java, the bytes don't change.

6. SBE vs JSON vs Protobuf vs FIX tag=value

JSONProtobufFIX tag=valueSBE
FormText, named keysBinary, tag + varintText, tag=value pairsBinary, fixed offsets
Read one fieldParse everythingScan tagsScan tagsOne memory read
AllocationMany objectsA message objectStringsNone (flyweights)
Same message, same bytesNot guaranteedNot guaranteed across versionsField order can varyYes
Human readableYesNoMostlyNo
Typical useWeb APIs, Kafka in PrayogMicroservices (gRPC)Broker and exchange connectivityExchange order entry, market data, journals

SBE is not a replacement for FIX; it is one of FIX's encodings. The same FIX business messages (NewOrderSingle, ExecutionReport) can be carried as text or as SBE. See the FIX page. For example, CME Group publishes its market data (MDP 3.0) and its iLink 3 order entry in SBE, and the Aeron messaging library uses SBE for its own protocols.

7. SBE in Prayog

WhereWhat
exchange-core/src/main/resources/sbe/journal-schema.xml6 commands (ids 1 to 6), 7 events (ids 20 to 26), the engine setup (id 40)
exchange-core/pom.xmlexec-maven-plugin runs sbe-tool during generate-sources; output lands in target/generated-sources/sbe
journal/JournalCodec.javaMaps records (NewOrder, Trade...) to the generated flyweights and back
JournalCodecTestRound-trip properties for every message, the pinned bytes, enum coverage
Parent pom.xml--add-exports java.base/jdk.internal.misc=ALL-UNNAMED for tests, because SBE's runtime (Agrona) uses the JDK's internal Unsafe. See off-heap memory.

Prayog's codec still builds a Java record per decoded message, so it is not fully garbage-free. The flyweights make that possible later if the S9 benchmarks show it matters.

8. Questions

Why must strings be read in order?

A string's position depends on the length of every string before it. The decoder keeps a running "limit" that advances as each one is read. Read the symbol before the client order id and you are reading the client order id's bytes as the symbol. One of S8's planted bugs did exactly this, and the round-trip test caught it.

If SBE is so fast, why does Kafka use JSON in Prayog?

Kafka carries events to other services (post-trade, leaderboard) written in other languages, where readability and JSON Schema validation matter more than microseconds. The journal is private to the engine and on the critical path. Different jobs, different formats.

What does "zero-copy" mean here?

The decoder reads fields directly out of the buffer the bytes arrived in (or the memory-mapped journal file). Nothing is copied into an intermediate object first.