Skip to content

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},
    },
})

bus.OnEvent("answers", 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

No persistence, no replay, no delivery guarantee beyond at-most-once, and no ordering between subscribers. Those are refusals rather than omissions: a bus that gains replay has gained state, and state turns a queue somebody has to size into a component somebody has to operate — backup, migration, corruption, capacity, a second thing to be down.

If you want those, they are a different component with a different name.

Where to go next

Design decisions live in spec 0001.

Early

Nothing is released. The backend abstraction has one real implementation, and an abstraction with one implementation is shaped entirely by that implementation, so the surface will move.