Skip to content

Cross-service bus + integration events

@nwire/bus is the contract for cross-process event delivery. @nwire/bus-nats is the NATS adapter. Both follow the same adapter pattern: contract package ships an in-memory default, adapter packages plug in opt-in.

When you need it

In a monolith, modules talk via local events — runtime.publish(event) fans out to every in-process subscriber synchronously. No bus needed.

The moment you split modules into separate services, the same event has to cross a network. The bus is what carries it.

The promise: module code doesn't change. Topology decides whether the event goes local or external.

Declaring an external need

ts
// inside the lessons module
defineModule("lessons", {
  actions: [advanceLesson],
  actors:  [Lesson],
  needs: {
    externalEvents: [
      { event: SubmissionWasGraded, from: "submissions" },
    ],
  },
})

At boot, createApp checks:

  • If the submissions module is loaded in this process → wire the in-process subscription.
  • If submissions is in a different service → wire a bus subscription on the topic submissions.submission-was-graded.

Topology

The deployment shape is data:

ts
// apps/topologies/split.topology.ts
export const splitTopology = {
  services: {
    "submissions-svc": { apps: ["submissions-app"], bus: { backend: "nats", url: process.env.NATS_URL } },
    "lessons-svc":     { apps: ["lessons-app"],     bus: { backend: "nats", url: process.env.NATS_URL } },
  },
}

apps/run.ts reads the topology, boots one service per declared shape, and wires the bus. Same modules; monolith if you point one service at all apps, split if you give each app its own service.

Integration event vs internal event

Internal eventIntegration event
defineEvent({ name: "submissions.answer-was-submitted" })Same definition
audience: ["product"]audience: ["product", "lessons"] (declares cross-BC use)
Local subscribers onlyLocal + bus subscribers

The event definition is identical; the cross-service contract emerges from the needs.externalEvents declarations of consuming modules.

Reliability

  • Events flow through an outbox (defineOutbox) before the bus, so a failed publish doesn't lose the fact.
  • Consumers use an inbox (defineInbox) for idempotency — replaying the same event is a no-op.
  • The bus adapter is responsible for at-least-once delivery; the inbox makes the consumer at-most-once-effectively.

See also

MIT licensed.