Something went wrong. Try again.
wip outbox library design
Something went wrong. Try again.
README.md
outpost #
Transactional outbox for Go. Durable messages, pluggable delivery, no driver types in the core.
Model #
Three seams: Appender (write side, bound to a unit of work the adapter owns),
Store (relay side: Claim/Complete/Fail), and Producer (what the relay
does with a claimed message). The contracts live in the package documentation;
this file is the tour.
Handlers are written once and registered by name; any producer can feed them.
// request path: same unit of work as the business write
err := outpost.Emit(ctx, myStore.InTx(tx), "email.send",
SendEmail{To: "ada@example.com", Template: "welcome"},
outpost.Key("ada"))
// worker path
d := outpost.NewDispatcher()
outpost.Handle(d, "email.send", func(ctx context.Context, e SendEmail) error {
info, _ := outpost.InfoFromContext(ctx)
return mailer.Send(ctx, render(e), mailer.IdempotencyKey(info.ID))
})
relay, err := outpost.NewRelay(store, outpost.ProducerFunc(d.Dispatch))
if err != nil {
return err
}
relay.Run(ctx)
Guarantees #
- At-least-once delivery. Consumers must be idempotent;
Message.IDis stable across retries and is the natural idempotency key. - Retries with configurable backoff, plus
RetryAfterfor provider rate limits andPermanentfor failures that should not be retried. Both delays are floors: the relay spreads each retry by up to 50% so independent workers do not stampede a shared provider. - Messages are abandoned, not retried forever, after
WithMaxAttempts. - The lease duration is derived from batch size, worker count, and delivery
timeout:
(ceil(batch/workers)+1) * deliveryTimeout. A claimed batch cannot outlive its lease while a worker is alive, so two workers cannot deliver the same message concurrently. The remaining duplicate window is a crash or hang between the provider accepting andComplete; no lease scheme closes that, which is what the provider idempotency key is for. - Stale leases cannot corrupt bookkeeping:
CompleteandFailare ignored unless the claim's token still matches.
Non-goals #
- Exactly-once delivery.
- Ordering: a single relay with
WithWorkers(1)processes claims in store order; nothing else is promised. - Codecs other than JSON.
- Deferred acknowledgements (a delivery that completes later, for example a
websocket client ack). The lease and token in
Claimleave room for it.
Adapters #
Not written yet. An adapter provides:
- something returning an
Appenderbound to its unit of work, for exampleInTx(tx),InBatch(batch),InTxn(txn); - a
Storeimplementation (Claim,Complete,Fail) honoring the contract on the interface; - optionally a
Producerfor its broker.