messaging¶
Two services need to talk. The obvious answer is a channel between them, and it works until the day one of them moves to another process, at which point the channel is a rewrite rather than a configuration change.
This module is the layer that makes that day cheap. A domain service knows two things: this module's
interface, and go/cloudevents. It never imports a
transport, so moving the carrier from a Go channel to an in-process broker to a cluster somebody else
operates changes wiring only.
bus, err := messaging.New(memory.New(), messaging.Settings{
Subscriptions: []messaging.SubscriptionSpec{
{
Name: "answers",
Pattern: "question.>",
Shape: messaging.Fanout,
Handler: func(ctx context.Context, d messaging.Delivery) error {
return answer(ctx, d.Event)
},
},
},
})
A bus, and a broker¶
A bus is the thing services put messages on. A broker is the server that carries them, and NATS is one. Naming them apart is deliberate rather than fussy: a system that holds both should not call them the same thing on the first day.
So services publish to a bus, the bus has a backend, and a backend may or may not talk to a broker. The in-memory one does not.
What you are buying¶
Four guarantees, and they are this module's rather than the backend's:
- Every subscription is bounded. Not configurable. An unbounded queue is not a policy; it is a decision to fail once memory is gone rather than at a number somebody chose.
- Every discard is counted, and attributed to the subscription it happened to. A discarded message is gone with no signal to whoever sent it, so the count is the only evidence it existed, and an aggregate answers "something was dropped" when the useful question is "which consumer is behind".
- Subscriber code never runs on a backend goroutine. That is what lets a panicking handler stop its own subscription instead of the process.
- The unit is a CloudEvent. A Go pointer handed down a channel does not survive a process boundary; a marshalled event does.
What it refuses, and what a backend can declare instead¶
The bus itself keeps nothing: no queue of its own, no replay, no state to operate. That is a refusal rather than an omission: a bus that gains state has gained a component somebody has to operate, meaning backup, migration, corruption, capacity, a second thing to be down.
What a backend can do beyond that, it declares in Capabilities and is held to by the
conformance suite: at-least-once delivery under a DurableName, storage that survives a restart,
a lease it can extend, wildcard patterns, per-subscriber ordering, replay from the oldest message
it holds. The in-memory backend declares almost none of it, honestly; a broker-backed one declares
what its broker earns. The guarantee under every one of them is that a failure is counted, not that
it did not happen.
If you want a delivery guarantee stronger than what a declared capability gives you, that is a different component with a different name.
Where to go next¶
- Getting started — publish and receive, with no broker.
- How-to — wire a bus into a controller, subscribe at runtime, handle a degraded construction, settle a delivery, scale a subscription with concurrency, hunt for handler panics.
- Reference — subjects and patterns, the backend contract.
- Explanation — a bus, not a broker, what happens at a full queue, what this module does not do.
Design decisions live on the wiki, starting with spec 0001.
Status¶
Released at v0.1.0, pre-1.0 and tracking its specs. Four provider modules implement the same
conformance suite this module ships:
messaging-nats (core NATS and JetStream),
messaging-sqs (FIFO and standard),
messaging-rabbitmq (quorum queues) and
messaging-amqp (AMQP 1.0, Artemis first). The
in-memory backend in this module talks to no broker at all, which is what makes
getting started possible with no infrastructure.