All notable changes to babelqueue-go are documented here.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.
The envelope wire format is versioned separately by meta.schema_version
(currently 1) — see the contract at babelqueue.com.
The new gdpr subpackage is the runtime half of GDPR sensitive-field governance
(ADR-0030) — the registry already declares/audits
x-gdpr-sensitive; this enforces it on the wire. It lives in the core module
(zero-dependency, stdlib only, like idempotency, schema and outbox), so it ships
with every producer/consumer. The crypto is a caller-provided Cipher interface
(KMS/Vault/HSM/tokenisation); only a stdlib AESGCMCipher reference ships — the core
pulls no crypto/KMS dependency (GR-7). The envelope stays frozen
(schema_version: 1): only the values of marked fields change (a sensitive value →
ciphertext string); trace_id is untouched and data stays pure JSON (GR-1/GR-3/GR-4).
This is the Go reference other SDKs mirror.
- GDPR field-encryption helper — a new
gdprsubpackage in the core module (github.com/babelqueue/babelqueue-go/gdpr, zero-dependency) that encrypts thedatafields a registry markedx-gdpr-sensitive, so PII never sits in cleartext on the broker (ADR-0030).-
gdpr.Cipher— the encryption seam the caller binds to a KMS/Vault/HSM or a tokenisation service (Encrypt(plaintext []byte) (string, error)/Decrypt(ciphertext string) ([]byte, error)). The core defines it and pulls in no crypto dependency (GR-7). -
gdpr.AESGCMCipher— a referenceCipherbuilt only on the Go standard library (crypto/aes+crypto/cipher+crypto/rand): AES-256-GCM with a fresh random nonce per call, prepended and base64-encoded so it drops straight into a JSON string. The key is the caller's; a wrong-key/tamperedDecryptfails GCM authentication rather than returning corrupt plaintext. -
gdpr.Protect(data, schema, cipher)/gdpr.Unprotect(data, schema, cipher)— standalone, opt-in producer/consumer helpers that encrypt/decrypt eachx-gdpr-sensitiveleaf in place. The sensitive paths come from the same per-URNschema.Schemathe validation path loads (schema.Schema.SensitivePaths), covering nested objects (profile.full_name) and array items (addresses[].line); an absent field is skipped, not an error. A wrong-keyUnprotectreturnsgdpr.ErrDecryptso the message takes the retry / dead-letter path. -
schema.Schemanow parses thex-gdpr-sensitiveextension keyword (booleantrueor a string category) and exposesSensitivePaths()— additive and validation-neutral: the keyword never makes a value valid or invalid, mirroringbabelqueue-registry's model, so annotating a schema is never a breaking change (GR-1).The envelope is unchanged (
schema_version: 1); this is purely additive (GR-6). The encrypted value is a JSON string, sodatastays pure JSON (GR-3) andtrace_idis preserved (GR-4).
-
The new …/asynq and …/machinery modules are published as the Go submodule tags
asynq/v1.0.0 and machinery/v1.0.0
(go get github.com/babelqueue/babelqueue-go/{asynq,machinery}); the core and other
transport modules are unchanged. asynq requires Go 1.24+ (its floor); machinery floors at
Go 1.23.
The new …/idempotency-postgres and …/idempotency-redis modules are persistent
idempotency.Store backends in their own submodules (own go.mod,
replace-based on the core like the transport submodules) — the core stays
zero-dependency (GR-7); a database/Redis driver is pulled in only if you import them.
Both floor at Go 1.21 (the core's floor): postgres on database/sql + pgx
v5.7.0, redis on the same go-redis/v9 the …/redis transport uses.
The new outbox subpackage is the Go port of the PHP BabelQueue\Outbox helper
(ADR-0029) — a transactional outbox in the core module
(zero-dependency, stdlib only, like idempotency and schema), so it ships with every
producer. The Store is an interface the caller binds to their own DB; only an
in-memory reference ships (no DB driver in the core, GR-7). The envelope is unchanged
(schema_version: 1); this is purely additive.
- transactional outbox helper — a new
outboxsubpackage in the core module (github.com/babelqueue/babelqueue-go/outbox, zero-dependency) that removes the producer dual write (ADR-0029): the message is persisted into the same database, in the same transaction, as the business write — so it commits or rolls back atomically with it — and a separate relay publishes the durable rows afterwards. The Go mirror of the PHPBabelQueue\Outboxhelper and the producer-side counterpart to the consumer-sideidempotencypackage.-
outbox.Store— the persistence contract the caller binds to their own DB (Save(encoded, queue) (id, err),FetchUnpublished(limit),MarkPublished(ids),MarkFailed(id, reason)). The core defines it and pulls in no DB driver (GR-7);FetchUnpublishedSHOULD lock/claim rows (FOR UPDATE SKIP LOCKED) in a concurrent adapter — documented as the adapter's job, not the in-memory reference's. -
outbox.Outbox.Write(env)— encodes via the frozen envelope codec (bytes stored verbatim, GR-1), captures the target queue frommeta.queue(else"default"), and delegates toStore.Save. It does not begin/commit anything — the caller owns the transaction boundary, which is the whole point. -
outbox.Relay—Flush(ctx)publishes one batch through the frozenTransportseam, marking each row published only after publish returns, or failed (caught,MarkFailed, left pending) with a bounded linear backoff (injectable sleeper); one poison row never blocks the batch.Drain(ctx, maxPasses)loopsFlushuntil a pass makes no progress (a safety ceiling). It publishes the stored bytes verbatim — never decoding/rebuilding the envelope — sotrace_idis preserved end-to-end (GR-4) and cross-SDK parity holds (GR-5). -
outbox.InMemoryStore— a process-local referenceStorefor tests / single-process demos (no real transaction; use a DB-backed adapter in production).The envelope is unchanged (
schema_version: 1); this is purely additive (GR-6).
-
- persistent idempotency stores (Postgres + Redis) — two new modules implementing
the unchanged core
idempotency.Storeinterface (ADR-0022), so a fleet of consumers shares one dedupe record keyed on the envelope'smeta.idinstead of every worker keeping its own in-memory set:…/idempotency-postgres(github.com/babelqueue/babelqueue-go/idempotency-postgres) — overdatabase/sql(pgx driver). The atomic claim isINSERT … ON CONFLICT (message_id) DO NOTHING/UPDATE … RETURNINGon the table's primary key: exactly one of N concurrent inserts wins (RETURNING proves it), a duplicate hits the conflict, and an expired row is re-claimed atomically by theDO UPDATE … WHERE expires_at <= now()branch. DDL ships asschema.sqland viaStore.Migrate(ctx).New(ctx, dsn)owns the pool;NewWithDB(db)borrows one;WithTable/WithTTLconfigure namespace and expiry.…/idempotency-redis(github.com/babelqueue/babelqueue-go/idempotency-redis) — reuses thego-redisclient the…/redistransport uses. The atomic claim isSET key value NX PX <ttl>: Redis evaluatesNXsingle-threaded so exactly oneSETcreates the key, duplicates see it present, and Redis expires it natively viaPX.New(url)owns the client;NewWithClient(client)borrows one;WithPrefix/WithTTLconfigure namespace and expiry.- Both add a non-interface
Claim(ctx, id) (bool, error)exposing the race result (true= first delivery / won,false= duplicate / parked);Rememberdelegates toClaimand discards the bool, soidempotency.Wrapaccepts either store unchanged. Unit tests are DB/broker-free (Postgres viago-sqlmock, Redis via a fake client seam); integration tests prove concurrent claims serialize, duplicates are rejected and TTL expiry reclaims, skipping cleanly withoutBABELQUEUE_TEST_PG/BABELQUEUE_TEST_REDIS. The envelope is unchanged (schema_version: 1); these are purely additive.
- machinery adapter — new
…/machinerymodule (github.com/babelqueue/babelqueue-go/machinery), a framework adapter over the Redis/AMQP-backed machinery task queue (not a new broker binding — its storage is Redis/AMQP, §1/§2); the sibling of the…/asynqmodule. machinery is argument-based, so the envelope URN (job) is the taskName(machinery routes on it like every BabelQueue consumer) and a single stringArgcarries the canonical envelope JSON byte-for-byte (no wrapping, no added fields). Produce:NewProducer(server).Dispatch(ctx, urn, data, …)(andSendfor a pre-built envelope);Signature/SignatureForexpose the*tasks.Signaturefor setting machinery-native fields (RetryCount,ETA,RoutingKey). Consume:Register(server, urn, handler)registers the handler under the URN as the task name, decoding the envelope arg and rejecting an unacceptable envelope withErrNotAccepted;Bind/Envelopeexpose the lower-level conversions. Unit- and conformance-tested against fakeSender/Registrarseams (no broker, no network). The envelope is unchanged (schema_version: 1); machinery is purely additive. Ships as a per-SDK MINOR. - asynq adapter — new
…/asynqmodule (github.com/babelqueue/babelqueue-go/asynq), a framework adapter over the Redis-backed asynq task queue (not a new broker binding — asynq's storage is Redis, §1). The canonical envelope JSON is the asynq taskPayloadbyte-for-byte (no wrapping, no added fields), and the envelope URN (job) is the asynq taskTypeName, so asynq routes by URN like every BabelQueue consumer. Produce:NewProducer(client).Dispatch(ctx, urn, data, …)(andEnqueuefor a pre-built envelope), with envelope options (WithQueue,WithTraceID) and asynq task options (MaxRetry,ProcessIn, …) mixed freely — distinguished by type. Consume:Register(mux, urn, handler)routes by URN on an*asynq.ServeMux, decoding the payload and rejecting an unacceptable envelope withErrNotAccepted;Bind/Task/Envelopeexpose the lower-level conversions. Unit- and conformance-tested against a fakeEnqueuerseam (no Redis, no network). The envelope is unchanged (schema_version: 1); asynq is purely additive. Ships as a per-SDK MINOR.
1.2.0 - 2026-06-13
The new …/azureservicebus module is published as the Go submodule tag
azureservicebus/v1.0.0 (go get github.com/babelqueue/babelqueue-go/azureservicebus); the
core, redis, amqp and sqs modules are unchanged. Requires Go 1.23+ (the Azure SDK floor).
- Azure Service Bus transport — new
…/azureservicebusmodule (github.com/babelqueue/babelqueue-go/azureservicebus), ababelqueue.Transportover the official Azure SDK (azure-sdk-for-go/.../azservicebus). Implements §4 of the broker-bindings contract: the canonical envelope is the messageBody, projected onto native fields (Subject= URN,CorrelationID=trace_id,MessageID=meta.id) plus thebq-application properties; PeekLock reserve →CompleteMessageack;attemptsreconciled tomax(body, DeliveryCount − 1). Options:WithConnectionString,WithAzureClient,WithClient,WithMaxWaitTime. Unit-tested against fake senders/receivers (no Azure, no network). The envelope is unchanged (schema_version: 1); ASB is purely additive. Ships as a per-SDK MINOR.
1.1.0 - 2026-06-12
The new …/sqs module is published as the Go submodule tag sqs/v1.0.0
(go get github.com/babelqueue/babelqueue-go/sqs); the core, redis and amqp
modules are unchanged at v1.0.0.
- Amazon SQS transport — new
…/sqsmodule (github.com/babelqueue/babelqueue-go/sqs), ababelqueue.Transportover the official AWS SDK (aws-sdk-go-v2). Implements §3 of the broker-bindings contract: the canonical envelope is theMessageBody, projected onto nativeMessageAttributes(bq-job/bq-trace-id/bq-message-id/bq-schema-version/bq-source-lang/bq-created-at); visibility-timeout reserve →DeleteMessageack;attemptsreconciled toApproximateReceiveCount − 1(never lowering a runtime-incremented count). Options:WithRegion,WithEndpoint(LocalStack/ElasticMQ),WithQueueURLPrefix,WithWaitTimeSeconds,WithVisibilityTimeout,WithFIFO,WithMessageGroupID,WithContentDedup,WithClient. Unit-tested against a fake SQS client (≈90%);-tags=integrationruns a LocalStack round-trip (covers the AWS config path). The envelope is unchanged (schema_version: 1); SQS is purely additive. Ships as a per-SDK MINOR.
1.0.0 - 2026-06-07
1.0.0 — the public API is now SemVer-stable: breaking changes require a MAJOR,
following the deprecation policy. The wire envelope is unchanged
(schema_version: 1). The …/redis and …/amqp transport modules are released as
v1.0.0 alongside and now require the core at v1.0.0. Full reference at
babelqueue.com.
- CI adds staticcheck (core +
redis/amqpmodules) and a >=90% core coverage gate (scripts/check-coverage.sh, currently 92%); added runtime tests coveringConsume/Run, the dead-letter options and the unknown-URN strategies. - GR-8 latency benchmark (
bench_test.go):BenchmarkEnvelopeRoundTrip/BenchmarkBarePayloadJSONplusTestCodecOverheadWithinBudget, which asserts the envelope encode/decode path adds ≤2% over plain-JSON serialization vs a conservative 750µs broker round-trip (measured ~0.3%; corroborated at ~1.4% over a live loopback Redis round-trip).
0.2.0 - 2026-06-06
- Runtime —
App(NewApp) withHandle,Publish,Consume/Runand a boundedDrain. Routes by URN over the canonical envelope; attempts-based retry → opt-in dead-letter queue; unknown-URN strategies (fail/delete/release/dead_letter). Options:WithDefaultQueue,WithMaxAttempts,WithUnknownURNStrategy,WithDeadLetter,WithDeadLetterQueue,WithDeadLetterSuffix,WithPollTimeout. - Transport abstraction —
Transportinterface +ReceivedMessage, with an in-processInMemoryTransport. The runtime stays zero-dependency; broker drivers ship as separate modules:github.com/babelqueue/babelqueue-go/redis— Redis transport (reliable BLMOVE + processing list), ongo-redis.github.com/babelqueue/babelqueue-go/amqp— RabbitMQ transport (durable queue, persistent delivery, contract AMQP properties,basic.get+ manual ack), onamqp091-go.
- The core module remains zero-dependency; only the
redis/amqpsubmodules pull a broker driver.
0.1.0 - 2026-06-06
Envelope/Metatypes andMake,Encode,Decode— build, render and parse the canonical{job, trace_id, data, meta, attempts}envelope (schema_version1). The single Go implementation of the wire format.Encodeproduces compact UTF-8 JSON with HTML escaping disabled — the canonical wire form (envelope frame byte-identical to the PHP and Python cores;datakey order followsencoding/json, JSON-insignificant).Envelope.URN()— resolve the URN (job, acceptingurnas an inbound alias).Envelope.Accepts()— consumer-side validation (rejects empty URN, unsupportedmeta.schema_version, blanktrace_id, missingdata).MakeoptionsWithQueueandWithTraceID(trace continuation).Annotate/DeadLetter— additivedead_letterblock builder.StrategyFail/StrategyDelete/StrategyRelease/StrategyDeadLetterunknown-URN strategy constants;ErrEmptyURN/ErrUnknownURN.- Shared cross-SDK conformance suite under
testdata/conformance/(vendored from the canonicalconformance/set) plus aTestConformancerunner.
- Pre-1.0: the public API may change before the
1.0.0tag. - Zero dependencies (standard library only); Go
>=1.21.