Context
BSFG facts must remain durable, replayable, hashable, and domain-agnostic. The object portion of a fact therefore needs a representation that is:
- stable under hashing and idempotency checks
- independent of any one domain’s closed type hierarchy
- easy for heterogeneous producers and consumers to emit and interpret
- compatible with optional schema governance without forcing transport-time validation
The design must decide whether object payloads are carried as strongly typed transport variants or as a canonicalized neutral data form.
Options Considered
| Option | Description | Benefits | Drawbacks |
|---|---|---|---|
| Typed protobuf oneof payloads | Encode each supported object shape as a protobuf oneof variant. |
- strong compile-time typing
- clear generated APIs
|
- closed variant set
- transport becomes domain-coupled
- schema churn propagates into boundary protocol
| | Arbitrary JSON text | Store object payload as non-canonical JSON string bytes. |
- flexible
- easy for producers to emit
|
- not stable for hashing
- equivalent objects may serialize differently
- weak idempotency foundation
| | Binary blobs only | Treat object payload as opaque bytes and leave interpretation entirely external. |
- maximal neutrality
- no JSON handling required
|
- poor inspectability
- harder debugging
- weak semantic interoperability
| | Canonical JSON bytes with optional schema reference (Selected) | Store object payload as canonical JSON bytes and allow an optional schema URI/version in the envelope. |
- stable hashing and idempotency
- domain-agnostic transport
- human-inspectable semantics
- schema governance possible without transport lock-in
|
- runtime validation is externalized
- producers must canonicalize correctly
- strong typing moves downstream
|
Decision
BSFG fact objects use canonical JSON bytes as the transport representation.
fact = (subject, predicate, object_json)
The object_json field is the canonical serialized form of the object payload. Producers and consumers may deserialize it into richer local types, but the boundary substrate treats it as canonical JSON bytes.
Schema governance is supported through an optional schema reference carried in the envelope, for example:
object_schema = "urn:bsfg:schema:batch.step_completed:v1"
BSFG does not require transport-time schema validation. Validation, compatibility checks, and stricter typing belong to producers, consumers, and domain services.
Consequences
Benefits:
- idempotency can rely on stable payload representation
- transport remains neutral across bounded contexts
- facts are inspectable without a closed generated-type universe
- schema governance can mature incrementally rather than being forced into the transport substrate
Tradeoffs:
- canonicalization errors become a producer responsibility
- runtime validation is not automatic at the boundary
- consumers that want strong typing must impose it themselves