Capability Matrix — what benzene-go does, deliberately doesn't, and how to fill the gap

Benzene is honest about its boundaries. This page is the single place that states, for each package or area of this port, what it provides (with the code that provides it), what it deliberately does not do (and why), and how to solve the rest outside Benzene. A gap that is a design decision is stated with its reasoning; a gap that is simply unbuilt says "not implemented" plainly — the two are never conflated here.

The one idea behind every row

Benzene abstracts at the business-logic boundary — you write a message handler once and host it anywhere — and never at the transport or storage boundary. If you wrap SQS behind a generic queue interface, you lose the SQS-specific capabilities that were the reason to choose SQS. So Benzene's answer to "does it abstract X?" is usually a deliberate no: every binding exposes the native message/event, and rolling your own middleware is a first-class, supported path, not an escape hatch.

Two corollaries specific to this port:

Core

Capability What benzene-go provides What it deliberately does NOT do (and why) How to solve the rest
Pipeline & routing Topic, Status, Result[T], Registry, Middleware/Pipeline, RouterMiddleware, three-phase App lifecycle (root module: app.go, router.go, registry.go, middleware.go) Reflection/attribute-scanning handler discovery — this port is explicit-registration by design (core-concepts.md §8's "explicit registry" idiom for Go) Register handlers explicitly on the Registry; the compiler is your discovery mechanism
DI DI-lite Container/Scope + ScopeFromContext (scope.go) Ship or integrate a reflection-based DI container — Go has no DI-container culture to bridge to Constructor wiring in main; the Container covers per-invocation scoping
Wire envelope & reserved names Transport-neutral envelope, ResolveMetadataTopic, ReservedNames — one injectable value for the configurable topic/correlation/version keys of wire-contracts.md §2 (wire/envelope.go, wire/metadata.go) Impose the envelope on transports that already carry routing (EventBridge detail-type) — the native key is the routing key there; Kafka and RabbitMQ read the topic header first and fall back to their native key (Kafka topic, routing key) Envelope-less transports use the Benzene envelope or a preset topic; otherwise route on the native key
Versioning Version selection per core-concepts.md §2 (router.go, registry.go, router_version_test.go): the version comes from the one benzene-version header (wire.DefaultVersionKey; renamed in one place via ReservedNames.VersionKey, which UseDefaultPipeline passes as WithVersionKey) or an HTTP {version} route segment (sugar for the header). A versioned message runs the exact (id, version) handler or is not-found naming topic and version; an unversioned one runs the topic's default handler — the unversioned handler, else the one registered with benzene.AsDefault(). A topic with only versioned handlers and no declared default is logged as a warning when the router is built (Registry.TopicsWithoutDefault). A version/x-version header is an ordinary header Transparent payload up/down-casting (versioning.md §4 Mechanism B) — not implemented: spec-optional, and the .NET implementation leans on reflection this zero-dependency port avoids; so the spec's cast-reachable selection step never applies. No "highest available" selector, even as an opt-in. Outbound clients do not write benzene-version from Topic.Version — not implemented Register a handler per supported version and declare the oldest one default; set the benzene-version header yourself on an outbound send; do payload up-casting in a small middleware if you need it
Validation Validated(validator, handler) typed handler wrapper + Validator[T]/Combine (validation/); validation.Pointer builds the RFC 6901 JSON Pointer each errors[].field carries (wire-contracts.md §1.3), from the request's JSON names (json tags) — runs the conformance:validate case A struct-tag/reflection validation DSL — the pipeline is type-erased until dispatch, so validation is a typed wrapper, not a pipeline middleware; and the wrapper cannot check a validator's Field is a pointer (the validator writes it) Plug any validation library inside your ValidatorFunc, converting its property paths with validation.Pointer
In-process dispatch inprocess — an in-process client.Sender over named pipelines (PipelineSet), for the modular-monolith pattern A container-wide outbound routing table (the .NET/TypeScript shape) — this port has no process-wide registry to hook into; a Sender is constructed and used directly like any other client Construct the in-process Sender in main and pass it where a transport client would go
Outbound error fidelity Every client that can receive a problem document rebuilds the Result's errors from it with message, field and code intact (core-concepts §12): httpclient.Client — from the failure envelope (envelope.ToResult, errors else detail as one opaque error) and, when the response is not an envelope, from an application/problem+json (or JSON) problem body, the status then from the problem's benzeneStatus (wire-contracts §4.1, so an application-defined status survives the hop; a success-class benzeneStatus on a failure is ignored) and from the reverse HTTP table only when it carries none — an envelope's own statusCode is that status already; awslambdaclient.Client — from the failure envelope (envelope.ToResult); inprocess.Sender — no wire, the handler's errors are passed through whole. Run end to end against problem-details-cases.json's envelopeCases by conformance/client_fidelity_conformance_test.go (httpclient over the envelope endpoint and over a route-mapped problem+json response, inprocess) and awslambdaclient/problem_conformance_test.go. A call that fails before any reply (no connection, refused or reset, the caller's deadline) is a failed Result, never a Go error, on every reply-receiving client — timeout when the deadline elapsed (the context's deadline, an error wrapping context.DeadlineExceeded, or a Timeout() bool error such as http.Client.Timeout's), else service-unavailable (transport-bindings §1; the shared rule is client.CallFailure): httpclient.Client, awslambdaclient.Client, inprocess.Sender (a context already done is not dispatched; a pipeline's Go error is mapped the same way) and grpcbinding.Client (grpc-go's own Unavailable/DeadlineExceeded through the reverse table; a conn's non-status error through client.CallFailure). The one-way clients (awssqs, awssns, awseventbridge, gcppubsubclient, azureservicebus, azureeventhub, azureeventgrid, azurequeuestorage, rabbitmq, kafka, awsstepfunctions) receive no reply and answer accepted: there is no request-reply queue client in this port. A one-way send that fails in the transport (the SDK or broker call errors before the broker accepts the message) is also a failed Result, never a Go error, through the same client.CallFailure: timeout when the deadline elapsed, else service-unavailable (each binding's TestClient_DeadlineExceededIsTimeout, Pub/Sub's table case). grpcbinding.Client reads the gRPC error model of wire-contracts §4.2 rather than a problem document: field survives (from BadRequest.FieldViolation.field, turned back into a pointer) and so does code (from FieldViolation.reason, §4.2) Attach the received problem document verbatim (the MAY of §12) — an application's own problem type reaches the caller only as the status and errors Read an application-defined problem type at the call site from the raw response, or carry it in an error code
Testing benzenetest in-process test host (Invoke[TReq, TRes]) — —

Transports — HTTP, gRPC, brokers

Area What benzene-go provides What it deliberately does NOT do (and why) How to solve the rest
HTTP inbound Native REST-style routing + envelope-over-HTTP (httpbinding/, shared dispatch in envelope/); portable CORS middleware (cors/). The envelope endpoint (EnvelopeHandler, /benzene/invoke) answers 200 for every processed envelope — not-found, deleted, a pipeline unauthorized alike — and 400 only for a body that is not an envelope; the inner problem omits status; WithTopicFilter restricts the topics it reaches (a restricted topic is not-found). A route mapped to a health topic answers 503 when the report says isHealthy: false (httpstatus.Response, shared with awslambda.HTTPHandler/azurefunctions.Handler) Own the HTTP server — the binding is an http.Handler you mount; host-enforced 401/403 before the envelope is read is your net/http middleware's job Mount on any net/http-compatible server/mux; wrap EnvelopeHandler in your auth middleware
HTTP outbound httpclient.Client (one Send), satisfying client.Sender; decorators WithCorrelationID/WithRetry (client/). Reads the response envelope whatever the HTTP status, through envelope.ToResult (isSuccessful decides; a missing one is a failure), and falls back to the reverse HTTP table only when the body is not an envelope — then rebuilding the errors, field and code intact, from a problem-document body (application/problem+json or JSON), and taking the status from its benzeneStatus when it carries one. A call that fails before any reply is timeout (deadline elapsed) or service-unavailable Manage connection pooling/auth for you Supply your own configured *http.Client
gRPC grpcbinding (own module): UnaryServerInterceptor claiming only routed methods, proto3-JSON bridging, benzene-status trailer, outbound Client; status tables in grpcstatus/; errors[].field pointers become dotted FieldViolation.field paths (/items/0/sku → items[0].sku) and the client turns them back into pointers; errors[].code is written to FieldViolation.reason and read back (wire-contracts §4.2) Streaming RPCs (client/server/duplex) — not implemented, a documented gap; and no codegen of service stubs — the binding claims routes, it doesn't own the server Register real protoc-generated services; unmatched methods fall through untouched
Kafka (self-hosted) kafka (own module, segmentio/kafka-go): consumer-group Consumer (one invocation + scope per record, explicit commits) + outbound Client; topic from the record's topic header (ReservedNames, inherited from the builder), falling back to the Kafka topic it was consumed from (transport-bindings' Kafka entry); other headers and the value verbatim; the Client produces to a Kafka topic named after the Benzene topic. Settlement: skip-and-continue by default (kafka.DefaultDelivery; a failed message goes to OnFailure and is committed past), or envelope.AtLeastOnce to re-dispatch it in place until it succeeds Broker-side redelivery/DLQ — Kafka has none Wire OnFailure to a dead-letter topic publish or log, or choose at-least-once and accept that a poison message stalls its partition
RabbitMQ rabbitmq (own module, amqp091-go): self-hosted Consumer + outbound Client — —
CloudEvents cloudevents/ — envelope↔CloudEvents 1.0 mapping + inbound HTTP handler (both content modes), zero-dep Ship per-broker CloudEvents transports — the mapping is the bridge; delivery is whatever CloudEvents-shaped system you point at it Point Event Grid, Knative, EventBridge, etc. at the handler

AWS

Full parity with the .NET reference on cloud integrations (see PARITY.md).

Area What benzene-go provides What it deliberately does NOT do (and why) How to solve the rest
Lambda runtime + HTTP awslambda/ — hand-rolled Runtime API bootstrap; HTTP adapter for Function URL / API Gateway v2, REST/v1, ALB (shape-detected per invocation), plus two envelope entry points: EnvelopeHandler for synchronous invokes (the caller reads the envelope) and AsyncEnvelopeHandler for Event invokes (a failure fails the invocation so Lambda retries) — the Runtime API does not reveal the invocation type, so the application picks The API Gateway custom-authorizer sub-feature is not implemented Attach an authorizer at the gateway; claims arrive as request context you can read in middleware
SQS awssqs (own module): inbound Lambda Handler (per-message batch-item failure) + outbound Client + self-hosted Consumer poller Hide SQS specifics — the native record is exposed Use SQS features (delay, FIFO, attributes) via the SDK directly
SNS / EventBridge / S3 awssns, awseventbridge (own modules — inbound handlers + outbound clients), awss3 (root module, inbound) Batch/partial-failure reporting on these — the platform offers none for direct async invokes; a failure returns a Go error so AWS's async-invoke retry applies (the same default as .NET's 1.0 settlement contract; per-binding detail in the settlement breakdown below) Handlers must be idempotent (at-least-once)
DynamoDB Streams / Kinesis / MSK Kafka awsdynamodb/, awskinesis/, awskafka/ (root module, zero-dep) — ordered CDC/stream handlers: sequential per partition, stop at first failure, report the resume position via batch-item failures Concurrent fan-out on ordered streams — ordering is the point of a stream; and no outbound side (writing the table/stream is the publish) Configure ReportBatchItemFailures on the event source mapping
Outbound invoke / orchestration awslambdaclient (Lambda invoke, RequestResponse/Event), awsstepfunctions (StartExecution, idempotent name) — own modules Run/resume workflows — starting an execution is Benzene's job; the workflow is Step Functions' (see Saga row) Model durable workflows in Step Functions itself
X-Ray Covered via diagnostics (OTel → X-Ray via ADOT/OTLP) A direct X-Ray-SDK port of .Aws.Lambda.XRay — not implemented; the OTel path is the intended route Export OTLP to ADOT

Azure

Custom-handler model: the Functions host POSTs a Data/Metadata JSON envelope; parity turns on whether a trigger reduces to that envelope (see PARITY.md §Azure).

Area What benzene-go provides What it deliberately does NOT do (and why) How to solve the rest
Functions triggers azurefunctions/ (zero-dep): HTTP Handler, QueueHandler (Storage Queue + Service Bus, topic resolution + outer-500 redelivery), CosmosHandler (fan-in change-feed batches), TimerHandler, EventGridHandler (Event Grid + CloudEvents schema), EventHubHandler (batch cardinality, per-event routing) Blob trigger: deferred by design — it is SDK-typed (BlobClient) in .NET, not a JSON the custom handler forwards; a faithful port must own the lease via azblob, and this repo does not fabricate an unverifiable custom-handler shape. Functions Kafka trigger: not implemented — zero-dep-achievable, but the exact custom-handler payload must first be pinned against a live host (same no-fabrication rule). Service Bus trigger settle is outer-status only (≈ AutoComplete) — explicit per-message settle needs the SDK For blobs, react to the Event Grid BlobCreated event via EventGridHandler and read the blob with azblob in your handler; for explicit Service Bus settle, use the self-hosted worker below
Self-hosted workers azureservicebus.Worker (explicit complete/abandon/dead-letter), azureeventhub.Consumer (caller-owned checkpoint), azurecosmos.Worker (caller-owned lease, fan-in) — own modules Own the Event Hub checkpoint store / Cosmos lease for you — checkpoint strategy is an application decision the worker surfaces rather than hides Wire the SDK's checkpoint/lease store of your choice
Outbound clients azureservicebus.Client, azureeventhub.Client, azureeventgrid.Client, azurequeuestorage.Client — own modules — —

GCP

Area What benzene-go provides What it deliberately does NOT do (and why) How to solve the rest
Cloud Functions Gen2 gcpfunctions (own module): RegisterHTTP + RegisterCloudEvent (functions-framework) — The zero-dep Cloud Run path needs no package at all — mount httpbinding
Pub/Sub gcppubsub/ — zero-dep push-subscription http.Handler (ack/nack via status code); gcppubsubclient (own module) — outbound publish Pull subscriptions on the zero-dep path — pull needs the SDK's streaming client; push is the zero-dep shape Use gcpfunctions.RegisterCloudEvent or run the SDK's pull client feeding the pipeline

Mesh, health, spec surface

Area What benzene-go provides What it deliberately does NOT do (and why) How to solve the rest
Mesh service-side mesh/ — the service registers its Contract Document, the same contract.Derive document benzene:spec serves, through mesh.Announcer (announce.go): Registration fills a detected placement (DetectPlacement) when the document carries none; runtime ("go"), profile (cloudservice.New's self-check) and degraded (events and sends when no OutboundRegistry was supplied) come from the derivation; no instanceId in the body. Heartbeats carry serviceVersion and the registered registrationHash, with the aggregate of healthcheck.Run over the service's checks; Run retries registration while the collector comes up and before each heartbeat until it lands. TraceMiddleware + outbound WithTraceContext, LogExporter/PushExporter trace feeds, issue emitter (IssueMiddleware/PushIssueExporter; each failing result's field/code error pairs — never the message — counted per issue and bounded to the ten most frequent, mesh.md §4.1) — every feed independent and optional at Core level. Meshed service (mesh.md §7.1): a Cloud Service (below) plus M1 — Announcer (register + heartbeats) and TraceMiddleware with a PushExporter (traces); the issue feed is outside M1 — and M2 — TraceMiddleware joins inbound traceparent and adopts its tracestate (dropped with a malformed one), and WithTraceContext forwards this span's traceparent with the tracestate unchanged on every outbound message: a send with a reply, a one-way send (every binding's Client is a client.Sender) and a published event (responseevents.NewSenderPublisher publishes through a Sender), replacing any forwarded copy. The app wires both; cloudservice.New reports them as not wired. There is no benzene:mesh topic, descriptor or descriptorHash (spec-0.4.0) Capture a language-native exceptionType on issues — the Go router converts a panic to a service-unavailable result before middleware sees it (the field is optional in mesh.md §4.1 for exactly this). Detect a region outside AWS Lambda — Azure Functions and Cloud Run document none in the environment, so none is guessed Status-based classification carries the signal; set contract.Info.Placement to name a region yourself
Mesh collector meshd/ — register/heartbeat/traces/issues ingest + benzene:mesh:query:* read models over an in-memory store with a bounded trace ring (meshd/meshd.go, store.go). A register body is a Contract Document read loosely: only service is required (no dialect check, schemas never interpreted); requests are the consumer claims, events ∪ sends the provider claims, benzene: entries ignored; a member that is absent or named in degraded is unknown — no edges, and no undeclared-edge contract-drift against it; a trace of a reserved benzene: topic (contract.IsReserved — benzene:spec, benzene:healthcheck, …) is never an undeclared edge, against the handler or the caller, since no document lists one (mesh.md §4.2). Each declared edge reports its providerActivity/consumerActivity entry, with lastObservedAt absent (not the zero time) when no trace has exercised it. Likewise a topic row's lastSeen is absent (not the zero time) for a declared topic with no traffic. The carried registrationHash is trusted (never recomputed, never a reason to reject); a heartbeat's hash is compared with the service's one registration (hashMatches). An issue's errors (field/code pairs) merge by identity under the ten-entry bound (mesh.MergeIssueErrors); a malformed entry is skipped, never failing the issue. A fleet flow carries its status — its first failure's, else its first event's. The fleet row's topics counts requests, produces counts events and sends, and it carries serviceVersion, runtime, transports and placement; missingFeeds' descriptor means "not registered"; the service view's descriptor is the registered document as sent. Passes mesh-collector-cases.json and mesh-issue-cases.json Durable storage — the collector is a diagnostic surface, not a system of record. Service Versions is not implemented: the catalog keys by service alone, so two side-by-side releases replace each other's registration, and mesh-service-version-cases.json/mesh-version-order-cases.json are listed opt-outs. The produced-vs-consumed version-skew read model is not implemented. Old topics/produces register bodies are not read Any port's collector can host this port's services over the shared benzene:mesh:* contract; use the .NET collector for side-by-side versions
Mesh View (UI) meshd.ViewHandler (meshd/view.go) — serves a Go-native embedded page (view.html, no JS framework, no external assets) at /benzene/fleet-ui, polling the collector's fleet query through the envelope endpoint. It does not embed or serve a canonical cross-language mesh-ui.html — no such asset is referenced in this repo; the page is this port's own rendering of the shared query contract A rich SPA — zero-dependency stance extends to the page The wire contract is the interop point; any UI can poll benzene:mesh:query:*
Health checks healthcheck/ — reserved-topic interception middleware + TCPCheck, HTTPPingCheck, DiskSpaceCheck (zero-dep; error categories, not raw messages, so internal hostnames/credentials don't leak). The result is ok whenever the checks ran; isHealthy carries the verdict (a warning does not flip it), and GET /benzene/health answers 503 when it is false. RouterMiddleware answers the reserved benzene:ping probe ok with no payload, so queue-borne probes are acknowledged. No benzene:liveness/benzene:readiness topics or routes are served Ship dependency-specific checks (database ping, broker liveness) — a check is a one-function interface Implement healthcheck.Check calling your dependency's SDK
Client-side contract check clienthealthcheck.ServiceCheck — asks a provider's benzene:spec for its Contract Document via an outbound Sender and compares the served contractHash with the one supplied by WithExpectedContractHash (a generated client's ContractHash), reporting the contract relationship (drift = warning, not a health flip). No provider-side or private hash: the check computes nothing itself Couple the check to the provider's transient health — benzene:spec is served health-independently on purpose, so contract drift and downtime stay distinguishable Use benzene:healthcheck for liveness; this check is a contracts diagnostic
Spec endpoint & profile Default paths per transport-bindings.md §1: /benzene/invoke, /benzene/spec, /benzene/health (httpbinding.EnvelopePath/SpecPath/HealthPath) and /benzene/fleet-ui (meshd.ViewPath), each configurable by mounting elsewhere; this port serves no spec-ui, mesh-ui or MCP surface. Self-description (R5): the contract package derives the Contract Document (contract-document.md, spec-0.4.0 shape: schemaDialect: "openapi-3.0", service/serviceVersion/versionScheme/description, the deployment-side runtime/placement/profile/degraded outside both hashes, requests/events/sends, no benzene: entries, an absent response for a handler returning struct{}, author examples only — none synthesized) at start-up with contract.Derive, which hoists every named struct into components (OpenAPI 3.0 dialect: nullable pointer to a named struct is the {"type":"object","nullable":true,"allOf":[…]} wrapper; format emitted for ints, floats, times and bytes), names it by BenzeneSchemaName() or its Go name, and fails before the first message on a component-name collision (*contract.NameCollisionError, naming both types, the topics and the remedy); undeclared generic instances are inlined. contract.Middleware answers benzene:spec ({type?, format?}; anything but benzene/json is not-implemented; WithDocumentType adds port extensions such as AsyncAPI), and httpbinding.SpecHandler serves GET /benzene/spec through the pipeline, copying the type/format query parameters. Both hashes — contractHash and registrationHash — are computed in the root module (contract.HashDocument) over the §6.2 projection, canonicalized by the hand-written RFC 8785 internal/jcs (differentially fuzzed against gowebpki/jcs in codegen/jcsfuzz). Events and sends are declared with contract.RegisterEvent/contract.RegisterSend (WithResponse for a reply); a (topic, version) in both is a start-up error. Cloud Service Profile (R1–R5, R7): cloudservice.New — one-call builder wiring the whole profile (R5: benzene:spec intercepted in the pipeline, answered wherever the transport carries a reply, and GET /benzene/spec), returning its derivation error or an honest ProfileReport: Requirements is the R1–R5/R7 checklist (Satisfied()/Unsatisfied()), which the document carries as its profile ({"name":"cloud-service"}, missing listing only R1–R5/R7 ids and omitted when none), and Mesh reports the meshed-service claim's M1/M2 apart (Meshed() requires the profile too); no Go consumer reads profile.missing, so a stale R6/R8 from an older service is ignored. cloudserviceprobe.Run — external black-box probe with tri-state verdicts for R1–R5/R7 (R5 needs both GET /benzene/spec and benzene:spec over the envelope endpoint), with Report.Mesh carrying M1/M2 as always Inconclusive (not observable from one service), noting any profile requirement observed unmet. A bodiless request on any Route mapped to benzene:spec — through httpbinding.Handler, awslambda.HTTPHandler or azurefunctions.Handler — carries its type/format query parameters as the request, as httpbinding.SpecHandler does cloudservice.New does not wire the meshed-service claim — M1's three feeds (registration, heartbeats, traces) or M2 (trace propagation) — they need a collector and exporter lifecycle the app owns; the report's Mesh section says so rather than overclaiming. The probe keeps its own path constants — it must audit any port's service, so it deliberately duplicates rather than imports Report.Mesh.Unsatisfied() is the exact to-do list to become meshed; run a mesh.Announcer over Service.Contract, wire mesh.TraceMiddleware and the push exporters, and wrap every outbound Sender (the event publisher's included) in mesh.WithTraceContext yourself
Contract docs & codegen openapi/ — OpenAPI 3.0 generation from the Contract Document (its components and $refs carried over as they are); asyncapi/ — AsyncAPI 3.0 generation from the Contract Document (requests → receive, with a reply only when a response is declared; events and sends → send), each with a GET Handler; codegen (own module: contractdoc + gengo + cmd/benzene-codegen) — typed-client generation from a Contract Document: rejects an unknown schemaDialect, a missing one included (missing-schema-dialect-is-rejected), ignores benzene: entries, include-list and topic-scoped projections recompute both hashes with the root module's contract.HashDocument, an absent response generates Result[struct{}], and the nullable wrapper becomes a pointer field. Passes every parseCases/topicScopeCases/schemaClosureCases case and every contract-hash-cases.json case, projections included Infer events or sends from call sites — they come only from RegisterEvent/RegisterSend declarations. Deriving responseevents mappings into event declarations is not implemented. Typed enums: Go cannot list a named string type's values, so enums derive as their base type Declare each event with contract.RegisterEvent; see docs/codegen-client.md for the client pipeline

Cross-cutting building blocks

Area What benzene-go provides What it deliberately does NOT do (and why) How to solve the rest
Idempotency idempotency/ — atomic-claim middleware over a pluggable Store; InMemoryStore (single-process, TTL); store outage fails open; a failure is never permanently suppressed Cross-instance de-duplication — independent processes can't coordinate without external shared state, and shared state can relocate the race, not remove it An external store with an atomic conditional write (DynamoDB conditional put, Redis SET NX) behind the Store interface, plus naturally idempotent handlers
Settlement (one-way bindings) Every one-way binding settles through envelope.Settlement (envelope/settlement.go), taken as WithSettlement by the handler constructors and as a Settlement field by the self-hosted workers. Its zero value is the spec default (transport-bindings §1, obligation 8): a message is acknowledged only when its invocation's isSuccessful is true (ignored included); a failure result, no result (unexpected-error), an unmatched topic (not-found), a middleware's Go error and a recovered handler panic are all redelivered — except the self-hosted kafka.Consumer, a stream that documents skip-and-continue as its default. Every binding offers the at-most-once override (envelope.AtMostOnce) and unmatched-topic-as-ignored (IgnoreUnmatchedTopics, keyed on benzene.IsUnmatchedTopic). Asserted against settlementDefaults by conformance/settlement_conformance_test.go. See the per-binding breakdown A single cross-transport acknowledgement abstraction — each binding settles with its transport's own signal (batch-item failure, Go error, non-2xx, nack, abandon, withheld delete/commit/checkpoint) Make handlers idempotent (idempotency/). Bound retries with the transport's redrive/DLQ settings
Outbox Not implemented. Nothing ships; the only trace is a seam — responseevents.Publisher is documented as implementable by "an outbox relay" (responseevents/responseevents.go). This is an unbuilt gap, not a design decision: the .NET reference ships Benzene.Outbox, and this port's ROADMAP records no "no" for it — Store-and-forward in your handler's own transaction against your store, relaying via a client.Sender; pair the consuming side with idempotency
Oversized payloads (claim check) Not implemented. No package, no seam. An unbuilt gap, not a design decision — the .NET reference ships Benzene.ClaimCheck — Offload to blob storage in a small middleware pair (offload before send, hydrate before deserialize), carrying the reference in a header
Resilience resilience/ (zero-dep): retry (full-jitter backoff, WithRetryOnError/WithRetryOnResult), cooperative Timeout, Bulkhead, Fallback; circuitbreaker (own module, sony/gobreaker/v2) with WithTripOnResult Forcibly stop a ctx-ignoring handler — impossible in Go; Timeout bounds the wait cooperatively. Hedging is not implemented — racing attempts needs per-attempt InvocationContext isolation the shared-ic pipeline contract doesn't provide, a core-concepts question deliberately not forced package-locally Honor ctx in handlers; for hedging, race at the caller (your own goroutines) until the contract question settles
Rate limiting ratelimiting/ — per-instance middleware over a Limiter interface + TokenBucket default; rejection = too-many-requests Fleet-wide (authoritative) limiting — N instances admit up to N× the rate; authoritative limiting belongs at the gateway Limit at the gateway/load balancer, or plug a shared-store Limiter
Caching cache/ — pluggable Store + read-through GetOrLoad[T]; InMemoryStore default; degrades safely (read error = miss, write error ignored) Ship a distributed cache — a shared store is its own module/dependency decision Implement Store over Redis/Memcached
Sagas saga/ — in-process staged orchestrator with LIFO compensation, SagaContext result threading, observability StateStore, clean-rollback-only RetryPolicy Durable crash-resume — steps are closures that can't be rehydrated; the state store records progress for observability, not recovery (matching Benzene.Saga's own boundary) Use a durable orchestrator (Step Functions via awsstepfunctions, Durable Functions, Temporal) for crash-durable workflows
Response events responseevents/ — post-handler republish of the response as a follow-up event (Map, CrudConvention, fan-out mappings; FailMessage default so a lost publish is never silently swallowed) The .NET package's build-time unmapped-response diagnostic and mapping-derived AsyncAPI send side — not implemented (the asyncapi generator exists; deriving its send side from these mappings is the open, unbuilt piece) Declare the published events with contract.RegisterEvent (the AsyncAPI generator reads them from the Contract Document); use LogAndContinue + OnPublishError where publish loss is acceptable
AuthN / AuthZ auth/ (zero-dep): Principal, BasicAuth (RFC 7617), BearerAuth — stdlib JWT validation with a strict algorithm allowlist (RFC 8725), per-family typed keys, iss/aud/exp/nbf/iat + skew; keys from StaticKeys or a caching JWKSResolver with OIDC discovery (auth/jwks.go: NewJWKSFromAuthority); Authorize/RequireRole/RequireScope Ship a policy-engine (OPA/Cedar) adapter; leak validation failure detail to callers (the real reason reaches only WithOnError) An authorization middleware calling your policy engine, built on Principal
Observability diagnostics (own module, OTel API only): one server span per invocation, W3C traceparent join, count/duration metrics, outbound WithTraceContext; logging/ — zero-dep log/slog per-invocation line Own the OTel SDK/exporter or ship vendor packages (Datadog, Zipkin) — the application owns the SDK; standard OTLP export covers vendors Configure the OTel SDK + OTLP exporter in main
Database / state access (nothing — by design) Any database/state-store abstraction — a database is not a transport; wrapping one hides its capabilities (the core anti-pattern) Your handler uses its own driver/SDK directly
Conformance conformance/ — runs this port against the vendored language-neutral fixtures from the main repo's docs/specification/conformance/ (conformance/testdata/, with its README.md). conformance/SPEC_VERSION pins the spec release the snapshot was taken from; the conformance-drift-check workflow fails when the snapshot is not byte-identical to that release and warns when a newer spec-* release exists. Contract-document producer (R5, and the register body): every derivationCases case (with its serviceInfo identity and placement), namingCases, specTopicCases and specHttpCases case and every contract-hash-cases.json case without a projection, through the runtime contract package; meshed service: mesh-trace-cases.json; collector: mesh-collector-cases.json and mesh-issue-cases.json against meshd (mesh-service-version-cases.json and mesh-version-order-cases.json are listed opt-outs — Service Versions is not implemented); client generator: codegen/conformance runs the client-generator groups and every hash case. Transport metadata (transport-metadata-cases.json): topicSources — every fixture binding is claimed in topicSourceClaims, and the non-metadata sources are driven (DynamoDB Streams' derived {tableName}:{eventName}, httpbinding's route, awskafka's Kafka-topic fallback here; EventBridge's detail-type, gRPC's method, RabbitMQ's routing-key fallback and kafka.Consumer's Kafka-topic fallback in their own modules); metadataCases and overrideCases (the consumed topic key absent from the headers, the case-insensitive read, the version selecting the handler through the real router) run through wire.ResolveMetadataTopic and through the real entry point of every native-metadata inbound binding, configured with UseReservedNames as a service would — gcppubsub.Handler, azurefunctions.QueueHandler (Service Bus) and EventHubHandler and awskafka.Handler here, awssqs.Handler/Consumer, awssns.Handler, azureservicebus.Worker, azureeventhub.Consumer, rabbitmq.Consumer and kafka.Consumer in their modules (shared harness internal/transportmetadata); settlementDefaults by settlement_conformance_test.go. The outbound clients' use of an overridden key is asserted by each client's own tests. Kafka (kafka.Consumer, awskafka.Handler) runs metadataCases/overrideCases too, and topicSources' Kafka-topic fallback is driven through both; the snapshot is pinned to spec-1.2.0 (conformance/SPEC_VERSION) Edit a fixture to make this port pass — the fixture is the neutral truth A mismatch is a bug here or a deliberate spec change there, never a fixture patch

Settlement — the per-binding breakdown

Re-audited against the source on 2026-10-11, after the spec-0.2.0 settlement work. "Failure" means the invocation ended with isSuccessful: false (validation-error, not-found, …), with no result (reported as unexpected-error), or with a middleware's Go error (service-unavailable). Every row inherits these facts:

Binding Package / file Shape (fixture id) Default on failure / no result / unmatched topic Uncaught middleware panic Unit of settlement
Lambda SQS trigger awssqs — sqs.go Handler queue (sqs) redeliver (batchItemFailures) redeliver — the runtime recovers it (awslambda/runtime.go invoke) and fails the invocation, so the whole batch is retried per message (messageId; needs ReportBatchItemFailures on the mapping)
SQS self-hosted poller awssqs — consumer.go Consumer queue (sqs) redeliver (not deleted; visibility timeout) redeliver — panics out of Run; nothing in the batch is deleted per message (DeleteMessageBatch of the acknowledged)
RabbitMQ consumer rabbitmq — consumer.go Consumer queue (rabbitmq) redeliver once (Nack with requeue), then nack without requeue (dead-letter exchange, or dropped); NoRequeue nacks without requeue on the first failure redeliver — panics out of Run; the broker requeues the unacked delivery per delivery
Pub/Sub push gcppubsub — gcppubsub.go Handler queue (pubsub) redeliver (HTTP 500) redeliver — no response (net/http recovers and closes the connection) per message
Functions Storage Queue / Service Bus trigger azurefunctions — queue.go QueueHandler queue (queue-storage, servicebus) redeliver (outer 500) redeliver — no response per message (invocation)
Service Bus self-hosted worker azureservicebus — worker.go Worker queue (servicebus) redeliver (AbandonMessage), or quarantine with AckModeDeadLetter redeliver — panics out of Run; the lock expires per message
Lambda SNS awssns — sns.go Handler one-shot (sns) redeliver (Go error → async-invoke retry) redeliver — invocation fails per invocation (one notification)
Lambda EventBridge awseventbridge — eventbridge.go Handler one-shot (eventbridge) redeliver (Go error) redeliver — invocation fails per event
Lambda S3 awss3 — s3.go Handler one-shot (s3) redeliver (Go error at the first failed record; under at-most-once it carries on) redeliver — invocation fails per invocation (the whole event is re-run)
Functions Event Grid trigger azurefunctions — eventgrid.go EventGridHandler one-shot (event-grid) redeliver (outer 500) redeliver — no response per event
Functions Timer azurefunctions — timer.go TimerHandler one-shot (timer) invocation fails (outer 500) — the function's retry policy applies where one is configured; a tick is not queued invocation fails — no response per tick
Lambda async envelope awslambda — envelope.go AsyncEnvelopeHandler one-shot (envelope-async) redeliver (Go error → async-invoke retry) redeliver — invocation fails per invocation (one envelope)
Lambda DynamoDB Streams awsdynamodb — dynamodb.go Handler stream (dynamodb-streams) redeliver from the failed record (a poison record too); at-most-once skips and continues redeliver — invocation fails (whole batch) per shard position (SequenceNumber)
Lambda Kinesis awskinesis — kinesis.go Handler stream (kinesis) redeliver from the failed record (a poison record too); at-most-once skips and continues redeliver — invocation fails (whole batch) per shard position (SequenceNumber)
Lambda MSK / self-managed Kafka awskafka — kafka.go Handler stream (kafka) redeliver from the failed offset, per partition; at-most-once skips and continues redeliver — invocation fails (whole batch) per partition position ({partition, offset})
Kafka self-hosted consumer kafka — kafka.go Consumer stream (kafka) skip-and-continue (documented default, kafka.DefaultDelivery): OnFailure, then committed; envelope.AtLeastOnce re-dispatches in place after ErrorBackoff until it succeeds redeliver on restart — panics out of Run; offset uncommitted per message, in partition order
Functions Event Hub trigger azurefunctions — eventhub.go EventHubHandler stream (eventhub) redeliver² (outer 500 at the first failed event) redeliver² — no response per batch (invocation)
Event Hub self-hosted consumer azureeventhub — consumer.go Consumer stream (eventhub) redeliver in place: the failed event and the rest of its batch are held and re-dispatched after ErrorBackoff before anything new is received; never checkpointed past redeliver on restart — panics out of Run; not checkpointed per event, in partition order
Functions Cosmos DB change feed azurefunctions — cosmos.go CosmosHandler stream (cosmosdb-changefeed) redeliver (outer 500, whole batch) redeliver — no response per batch (fan-in: one invocation)
Cosmos DB self-hosted worker azurecosmos — worker.go Worker stream (cosmosdb-changefeed) redeliver (token not advanced; re-read after ErrorBackoff) redeliver on restart — panics out of Run per page (continuation token; fan-in)
Cloud Functions Gen2 CloudEvent gcpfunctions — gcpfunctions.go RegisterCloudEvent cloudevents (one-shot) redeliver² (returns an error) redeliver² — functions-framework v1.9.2 re-panics; cloudevents sdk-go v2.16.2 recovers it into an error result per event
CloudEvents over HTTP cloudevents — http.go Handler cloudevents (one-shot) redeliver (HTTP 500) redeliver — no response per event (batched mode answers 415)

² The binding fails the invocation. Whether the platform then redelivers depends on the trigger's retry configuration, which this repo cannot verify against a live host. In particular, how the Functions host checkpoints an Event Hub batch after a failed invocation is unverified.

Request-response bindings settle by responding (fixture shape request-response): httpbinding, awslambda.HTTPHandler, azurefunctions.Handler, grpcbinding, gcpfunctions.RegisterHTTP, and the envelope over a synchronous carrier (httpbinding.EnvelopeHandler, awslambda.EnvelopeHandler). The Functions Kafka and Blob triggers (blob-storage in the fixture) and Pub/Sub pull are not implemented (see the rows above). cloudevents.Handler and gcpfunctions.RegisterCloudEvent have no settlementDefaults id; the conformance runner reports them rather than inventing one.

Divergences from the .NET settlement contract

Deliberately out of scope (a "no", not a gap)

.NET-ecosystem idioms with no Go analogue to port (see ROADMAP.md for the full reasoning): alternate DI containers (Container/Scope is the Go idiom), alternate loggers (log/slog is standard), alternate serializers (encoding/json is idiomatic; Avro/MessagePack would be new dependency decisions, not ports), vendor-specific observability packages (OTLP covers them), and ASP.NET-on-Lambda/Functions hosting bridges (net/http handlers are the pipeline here).

Why "we don't do that" is a feature

Every deliberate "no" above buys you something: you keep the full power of the tool you chose, you're never blocked by a leaky abstraction, and the zero-dependency core stays small and stable. When you need something benzene-go doesn't ship, the extension model — a custom Middleware, a custom Check, a Store/Limiter/Sender implementation — is the supported, documented path. See docs/middleware.md.