Flyweight vs Whole-Struct
You can work field-by-field (classic flyweight) or fill / materialise a whole struct. Use the style that matches how much of the message you touch.
| Style | Best when | Cost | Schema evolution |
|---|---|---|---|
| Flyweight (per-field) | You only read one or a few fields; hot path | Zero-copy; no heap | New fields are optional at call sites (you simply don’t read them) |
*FixedFields + .fixed(...) | You always write the entire fixed block | One struct write, still flyweight buffer | Adding a required fixed field to the schema → compile error until you set it in the struct |
*Domain DTO (.with_domain_objects(DomainVarData::…)) | Whole message as owned data; enum picks String vs Vec<u8> var-data | Allocates — never use on the hot path. Easier app code for tooling, logging, offline processing | Same idea: regenerating after a schema change forces you to fill new struct fields |
Encode — whole fixed block as a struct
When you always populate every fixed field, a struct is clearer and schema additions break at compile time:
#![allow(unused)] fn main() { let mut buf = [0u8; HeartbeatEncoder::compute_length_with_header()]; // Buffer is exact size from const compute_length_with_header — no bounds check needed. let len = HeartbeatEncoder::try_wrap_and_apply_header(&mut buf, 0)? .fixed(&HeartbeatFixedFields { sequence: 7, timestamp: 0 }) .encoded_length_with_header(); let dec = HeartbeatDecoder::try_from(&buf[..len])?; assert_eq!(dec.sequence(), 7); }
(From book/examples/heartbeat-encode.rs — compiled against the feature-tour codec.)
If the schema later adds a required field to the fixed block, this stops compiling until you add it to the struct literal — you cannot silently omit it.
Decode — individual fields (flyweight)
#![allow(unused)] fn main() { // Encode a sample car first (normally bytes come from the wire) let complete_len = CarEncoder::compute_length() .fuel_figures_ragged(0, |_| Ok(()))? .performance_figures_ragged(0, |_| Ok(()))? .manufacturer(5)? .model(5)? .activation_code(3)? .encoded_length_with_header(); const PAD: usize = 256; assert!(complete_len <= PAD, "car length {complete_len} exceeds pad {PAD}"); let mut storage = [0u8; PAD]; let buf = &mut storage[..complete_len]; let fields = CarFixedFields { serial_number: 1234, model_year: 2013, available: true.into(), code: Model::A, some_numbers: [10, 20, 30, 40], vehicle_code: *b"ABCDEF", extras: OptionalExtras::default(), engine: Engine::new(2000, 4, *b"123", 0i8, false.into(), Booster::new(BoostType::TURBO, 210)), }; let n = CarEncoder::try_wrap_and_apply_header(buf, 0)? .fixed(&fields) .fuel_figures(0, |_| Ok(()))? .performance_figures(0, |_| Ok(()))? .manufacturer(b"Honda")? .model(b"Civic")? .activation_code(b"abc")? .encoded_length_with_header(); assert_eq!(n, complete_len); // Now decode — read only the fields you need, no DTO allocation: let car = CarDecoder::try_from(&buf[..n])?; assert_eq!(car.serial_number(), 1234); assert_eq!(car.model_year(), 2013); assert_eq!(car.code(), Model::A); assert_eq!(car.engine().capacity(), 2000); }
(From book/examples/flyweight-access.rs — compiled against the feature-tour codec.)
Decode — whole message as a DTO
Do not use on the latency-sensitive path. DTO decode allocates
Vec/Stringand copies every field. For the hot path, use the flyweight decoder instead.
When you always need (almost) everything, or want to pass a value across threads / into non-SBE code:
#![allow(unused)] fn main() { pub fn demo_car_domain_dto(wire: &[u8]) -> Result<(), Box<dyn std::error::Error>> { let dec = CarDecoder::try_decode(wire, 0)?; // try_from_decoder (not TryFrom/From): two fallible sources — decoder vs // try_from_slice_with_header for framed bytes; materialisation can fail. let dto = CarDomain::try_from_decoder(dec)?; assert_eq!(dto.serial_number, 1234); assert!(dto.available); // bool domain field assert_eq!(dto.fuel_figures.len(), 2); assert_eq!(dto.fuel_figures[0].usage_description, "Urban"); assert_eq!(dto.manufacturer, "Honda"); const RE_PAD: usize = 512; assert!(wire.len() <= RE_PAD); let mut storage = [0u8; RE_PAD]; let n = dto.encode(&mut storage[..wire.len()])?; assert_eq!(&storage[..n], wire, "DTO re-encode must be byte-identical"); Ok(()) } }
Rule of thumb: one field on the hot path → flyweight. Always fill or
always consume the whole message → FixedFields / Domain for clarity and
compile-time breakage on schema growth. More on DTOs in
Recipes — Domain DTOs.