Context
The BSFG ADR set defines the boundary topology (ADR-0001, ADR-0002), durability guarantees (ADR-0003, ADR-0004), replay semantics (ADR-0006, ADR-0033), message ontology (ADR-0008, ADR-0020, ADR-0021, ADR-0037), and the runtime API contract (ADR-0022). These decisions establish what BSFG guarantees and how its internal mechanisms behave.
However, application developers integrating with BSFG require an explicit contract specifying:
- what Emitters (Producers) must implement to interact correctly with the boundary
- what Consumers must implement to retrieve and process facts safely
- retry expectations under at-least-once transport
- artifact lifecycle obligations before and after fact emission
Without this explicit contract, integrators may incorrectly assume:
- synchronous RPC semantics — that
AppendFactconfirms business acceptance - exactly-once transport — that retrying an append is unsafe or redundant
- implicit coordination across zones — that the boundary manages delivery acknowledgment end-to-end
These misassumptions lead to integration defects: duplicate fact emission without stable IDs, premature business-outcome confirmation, or consumer-side processing without idempotency. An explicit integration contract removes this ambiguity.
Options Considered
| Option | Description | Pros | Cons |
|---|---|---|---|
| Implicit contract | Leave emitter and consumer behavior implied by the existing ADRs. |
- minimal documentation surface
- no new normative text
|
- integrators must synthesize obligations from multiple ADRs
- common mistakes (non-idempotent consumers, unstable IDs) remain undocumented
- no single normative reference for integration testing
| | API-only documentation | Describe only the four RPC operations — inputs, outputs, and error codes — without specifying operational obligations. |
- easy to generate from the protobuf definition
- familiar format for API consumers
|
- omits retry obligations, idempotency requirements, and artifact sequencing
- does not distinguish between emitter and consumer roles
- leaves failure and recovery semantics undefined
| | Explicit integration contract (Selected) | Define emitter and consumer obligations normatively: required call sequences, idempotency guarantees, retry expectations, and artifact lifecycle. |
- clear operational guidance for integrators
- failure and retry semantics become explicit and testable
- avoids incorrect assumptions about transport guarantees
- single normative reference for both roles
|
- slightly larger documentation surface
- integrators must implement idempotent consumers
|
Decision
BSFG defines an explicit integration contract covering both actor roles. This contract is normative: all systems interacting with the boundary via the four RPC operations must satisfy it.
Producer (Emitter) Responsibilities
An Emitter (Producer) is any system that appends facts to a BSFG boundary node via AppendFact. Producers must satisfy the following obligations:
- Generate a stable
message_id. The identifier must be derived deterministically from the business event — not from wall-clock time, random generation, or sequence allocation that resets on restart. The same business event must always yield the samemessage_id. - Treat
AppendFactas retry-safe. The boundary enforces idempotent append (ADR-0003, ADR-0033). A producer that receives a network error or timeout must retry with the samemessage_idand identical payload. Re-issuing is safe; generating a new ID for a retry is not. - Never mutate facts once emitted. Facts are append-only (ADR-0014). A producer may not modify or retract a previously appended fact. Corrections are new facts.
- Upload artifacts before referencing them. If a fact references a large binary artifact, the producer must complete
PutObjectsuccessfully before issuingAppendFact. Consumers retrieve artifacts on demand; a missing artifact at retrieval time is a producer defect. - Tolerate asynchronous propagation.
AppendFactconfirmation means the fact is durably stored at the ingress boundary. It does not mean the fact has been delivered to or processed by any consumer. Producers must not block on downstream acknowledgment.
Required call sequence:
optional PutObject — upload artifact if referenced AppendFact — emit fact with stable message_id retry AppendFact on failure (same message_id, same payload)
Producer guarantees to the boundary:
- the same
message_idis used for all retry attempts of the same business event - the payload is identical across retries for the same
message_id - any referenced artifact exists and is immutable before the fact is appended
Consumer Responsibilities
A Consumer is any system that retrieves facts from a BSFG boundary node via FetchFacts and ConfirmReceipt. Consumers must satisfy the following obligations:
- Retrieve facts using
FetchFacts. Consumers drive their own fetch progress via durable named consumers (ADR-0023). Fetch is pull-driven by the receiving side (ADR-0032); consumers must not assume push delivery. - Process facts idempotently. At-least-once transport (ADR-0033) means any fact may be delivered more than once, including after replay from boundary reconnection. A consumer must produce the same business outcome regardless of how many times a given fact is processed. Idempotency is the consumer's responsibility, not the boundary's.
- Persist business outcome before confirmation. The consumer must durably record the result of fact processing before calling
ConfirmReceipt. Confirming before persisting creates a gap: the boundary advances the cursor, but the business outcome is lost on consumer restart. - Confirm processing using
ConfirmReceipt. Confirmation advances the durable consumer cursor. A consumer that fetches but never confirms will re-receive all facts from the last confirmed position on restart. - Tolerate delayed or replayed facts. Facts may arrive after connectivity restoration or boundary replay. Consumers must not assume facts arrive in wall-clock order relative to business events at the source zone.
Required call sequence:
FetchFacts — retrieve next batch from durable consumer position apply fact — execute idempotent business logic persist result — durably record business outcome ConfirmReceipt — advance cursor at the boundary
Consumer guarantees to the boundary:
- business logic is idempotent: re-processing the same fact produces the same outcome
ConfirmReceiptis called only after the business outcome is durably persisted- delayed and replayed facts are tolerated without consumer-side errors
Artifact Handling
Large binary artifacts are stored out-of-band and referenced by facts (ADR-0013, ADR-0024, ADR-0039). The artifact lifecycle spans both roles:
Producer sequence:
PutObject — upload artifact to object store AppendFact — emit fact referencing artifact by metadata
Referenced artifacts are immutable once fact-addressed (ADR-0039). A producer may not overwrite or delete an artifact that has been referenced by an appended fact.
Consumers must tolerate delayed artifact availability. Object store propagation may lag fact propagation across zones. Consumers should retry artifact retrieval before treating unavailability as a permanent error.
Delivery Semantics
The boundary provides the following transport guarantees:
- At-least-once transport — facts are delivered one or more times; never zero times after successful append
- Idempotent append — duplicate
AppendFactcalls with the samemessage_idare safely absorbed - Replay-based recovery — boundary reconnection replays facts from the confirmed frontier; no facts are skipped
Application layers must not assume exactly-once delivery. The boundary does not guarantee that a consumer processes each fact exactly once. That property is achieved by the combination of at-least-once delivery and idempotent consumer processing.
Consequences
Benefits:
- integrators know exactly what to implement for each actor role
- failure and retry semantics are explicit and testable
- incorrect assumptions about exactly-once transport are ruled out by normative text
- the producer–consumer contract is a single stable reference for integration testing and auditing
Tradeoffs:
- documentation surface is slightly larger than API-only reference
- integrators must implement idempotent consumers — this is a requirement, not a recommendation