AnnouncementsIntroducing Arkoren

    Menu

    Broadcasting Overview

    Announce typed facts and route received notifications to independent listeners.

    What's on this page
    1. Distinguish fan-out notifications from queued work and in-process events.
    2. Publish typed order facts and register exact or wildcard listeners.
    3. Separate transport acceptance from active subscriptions and recipient effects.
    4. Assert publication requests without claiming broker delivery.
    On this page

    When an order is created, a search index and a dashboard can both need to hear about it. A broadcast announces that fact without making the publisher know which services listen. The publisher sends once; the transport determines which subscribers receive a copy.

    Arkoren separates publishing from receiving. A broadcast.Topic[T] names the fact and fixes its payload type. A Publisher JSON-encodes that payload; a Consumer routes received messages to registered listeners. A driver connects these facades to an in-process broker or an external service. Constructing a facade neither starts nor takes ownership of its driver.

    Choosing Broadcasts

    Use broadcasts for notifications whose recipients can recover current state, such as refreshing an order index. Most broadcast drivers do not retain messages for offline subscribers. A successful publish confirms transport acceptance, not that any recipient handled the notification.

    Use a queue to dispatch work to competing workers with a retry policy, such as generating an invoice. Use an event to notify listeners within the same process. Broadcasting is not a substitute for either a durable work queue or a direct in-process call.

    Independent subscribers ordinarily each receive a copy. Replicas do not automatically share that copy: use a NATS queue group, or an SNS subscription with one SQS queue per service. Replicas of a service then compete on its queue; independent services retain separate subscriptions.

    Defining Topics

    Publish an order identifier rather than a complete private order record when recipients can load the authorized data themselves. Share the topic declaration and JSON contract between publisher and listener:

    import "arkoren.dev/core/service/broadcast"
    
    type OrderCreated struct {
    	OrderID string `json:"order_id"`
    }
    
    var OrderCreatedTopic = broadcast.NewTopic[OrderCreated]("order.created")

    A name contains dot-separated tokens, each 1–64 lowercase letters, digits, or hyphens, without a leading or trailing hyphen. The complete name is at most 255 bytes. Topic construction retains the name without validation; publication and registration return an error matching broadcast.ErrInvalidName for invalid names. They do not normalize names.

    Topics sharing a name must agree on JSON payload semantics, even when services declare them separately. AsyncAPI can describe this wire contract; generating a document does not configure subscriptions.

    Publishing Facts

    With an existing driver from Drivers and operation context ctx, wrap the publishing side and announce the committed order:

    publisher := broadcast.NewPublisher(driver)
    
    err := publisher.Publish(ctx, OrderCreatedTopic, OrderCreated{
    	OrderID: "o-1042",
    })

    The envelope carries name order.created and JSON payload {"order_id":"o-1042"}. A nil error means acceptance by the driver, not listener completion or retention. Encoding errors occur before transport access; driver errors retain their causes. Focused snippets leave routine error results visible; your application must handle them.

    With non-retaining drivers, publishing before subscribers are active loses the notification. Driver setup and listener registration alone do not establish an active subscription.

    Commit the order before publishing it. The commit and publish are separate operations: a crash between them can lose the notification. If that loss is unacceptable, persist the intent to publish alongside the order and arrange recovery. Retrying an uncertain publication can duplicate a notification.

    Listening

    In a receiving service, use a driver connected to the publisher’s scope, such as the same Redis prefix or RabbitMQ exchange. SNS instead uses a subscribed SQS queue. With your application’s search indexer, register a typed listener:

    consumer := broadcast.NewConsumer(driver)
    
    unsubscribe, err := consumer.Listen(OrderCreatedTopic,
    	func(ctx context.Context, order OrderCreated) error {
    		return search.Index(ctx, order.OrderID)
    	})
    if err != nil {
    	return err
    }
    defer unsubscribe()

    An accepted delivery decodes into OrderCreated, then calls the indexer with o-1042. Keep the registration alive for as long as you need it; the deferred unsubscribe belongs to a scope that outlives consumption, not a short-lived setup function.

    Registration does not subscribe to the broker. Run the consumer in your application’s supervised scope:

    err := consumer.Run(ctx)

    Run blocks until cancellation or a driver error. Starting a goroutine or registering an application runner is not a subscription-ready signal. For non-retaining drivers, establish active consumption before publishing a notification you need to observe. Drivers owns connection and cleanup setup.

    Listener errors do not reach the publisher, and they do not by themselves end Run. The driver decides whether to retry. A retry can invoke successful listeners again, so make repeated notifications safe. See Message Delivery for cancellation, ordering, and failure handling.

    Matching Names

    To observe several order facts, register a pattern listener. It receives the complete envelope rather than a decoded application value. With an existing logger, record only its routing name:

    unsubscribe, err := consumer.ListenPattern("order.#",
    	func(ctx context.Context, message broadcast.Message) error {
    		logger.InfoContext(ctx, "order notification", "name", message.Name)
    		return nil
    	})
    if err != nil {
    	return err
    }
    defer unsubscribe()

    order.# matches order, order.created, and order.created.eu. A final # matches zero or more tokens; * matches exactly one. Thus order.* matches only the middle example. Wildcards must fill whole tokens, and # must be last: order.cre* is invalid.

    Every matching listener runs, including an exact listener and a pattern listener on the same consumer. Raw payload bytes and metadata are shared between these listeners; do not mutate them. Avoid logging their contents. The monitoring guide explains metadata and privacy.

    Testing Publications

    To check that application code announces an order without running a broker, use the recording fake. In a test with t *testing.T, assume CreateOrder accepts the publisher and publishes the topic above after saving the order:

    import "arkoren.dev/core/service/broadcast/broadcasttest"
    
    fake := broadcasttest.New()
    publisher := broadcast.NewPublisher(fake)
    
    if err := CreateOrder(t.Context(), publisher, "o-1042"); err != nil {
    	t.Fatal(err)
    }
    fake.AssertPublished(t, OrderCreatedTopic, func(order OrderCreated) bool {
    	return order.OrderID == "o-1042"
    })

    This proves the publication request and payload, not broker delivery. To exercise registered listeners, fake.Drain(t.Context(), consumer) routes recorded messages synchronously and returns joined listener errors without redelivery. The testing guide explains why requesting work and executing its effects need separate assertions.

    For every function and option, see the broadcast API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive