Skip to content

defineModule

defineModule(name, manifest) declares a bounded context — a related set of actions, events, actors, projections, queries, workflows, orchestrator primitives, and HTTP routes.

Signature

ts
function defineModule(name: string, manifest: ModuleManifest): ModuleDefinition

ModuleManifest

ts
interface ModuleManifest {
  // Domain primitives
  actions?: readonly ActionDefinition[]
  events?: readonly EventDefinition[]
  actors?: readonly ActorDefinition[]
  handlers?: readonly HandlerDefinition[]
  projections?: readonly ProjectionDefinition[]
  queries?: readonly QueryDefinition[]
  workflows?: readonly WorkflowDefinition[]

  // Orchestrator primitives
  externalCalls?: readonly ExternalCallDefinition[]
  inboundWebhooks?: readonly InboundWebhookDefinition[]
  outboxes?: readonly OutboxDefinition[]
  inboxes?: readonly InboxDefinition[]
  crons?: readonly CronDefinition[]

  // HTTP wiring
  routes?: RouteDefinition

  // Cross-module dep graph (validated at createApp)
  needs?: {
    events?: readonly EventDefinition[]               // events I subscribe to from elsewhere
    externalEvents?: readonly EventDefinition[]       // events from other services (via bus)
    actions?: readonly ActionDefinition[]             // actions I dispatch in other modules
  }

  // Studio-aware
  description?: string
  owners?: readonly string[]
  journey?: readonly { id: string; label: string; description?: string }[]
}

Example

ts
export const submissionsModule = defineModule("submissions", {
  description: "Submission lifecycle: receive → grade → review.",
  owners: ["curriculum-eng"],
  journey: [
    { id: "J3-submit-exercise", label: "Submit an answer" },
    { id: "J4-auto-grade-fallback", label: "Auto-grader flags low-confidence" },
    { id: "J5-grade-submission", label: "System grades a submission" },
    { id: "J6-review-flagged", label: "Reviewer dispositions a flagged submission" },
  ],

  // Visibility is module-level: `.public()` in the manifest exposes an
  // item to other modules; without it, the item is private to submissions.
  actions: [
    submitAnswer.public(),
    flagForReview.public(),
    gradeSubmission.public(),
    reviewSubmission.public(),
    sendReviewReminder,                              // internal — no .public()
  ],
  events: [
    AnswerSubmittedEvent,                            // domain — private
    SubmissionAutoGradedEvent.public(),              // integration — crosses BC
    SubmissionFlaggedForReviewEvent.public(),
    SubmissionManuallyGradedEvent.public(),
  ],
  actors: [Submission],
  projections: [SubmissionsByStudent],
  queries: [submissionsByStudent.public()],
  workflows: [autoGradeWorkflow],
  externalCalls: [notifyStudent],

  routes: defineRoute({
    "POST /submissions": submitAnswer,
    "GET /submissions": submissionsByStudent,
    "POST /submissions/:submissionId/flag-for-review": flagForReview,
    "POST /submissions/:submissionId/grade": gradeSubmission,
    "POST /submissions/:submissionId/review": reviewSubmission,
  }),

  needs: {
    // submissions consumes no events from other modules — but its
    // public events feed mastery, lessons, etc.
  },
})

needs — the explicit dep graph

createApp walks every module's needs at boot and validates:

  • ✅ Every needed event is provided by another module
  • ✅ The provider declares it visibility: "public"
  • ❌ Self-reference ("submissions needs an event submissions provides") → throws
  • ❌ Internal events can't be subscribed cross-module → throws
  • ❌ Provider missing → throws

This catches typos + refactor breakage before the first dispatch.

ts
// mastery's module declares cross-bc dependencies
defineModule("mastery", {
  needs: {
    events: [SubmissionAutoGradedEvent, SubmissionManuallyGradedEvent],
  },
  workflows: [updateOnGradedWorkflow, updateOnReviewedWorkflow],
})

Now createApp({ modules: [submissionsModule, masteryModule, ...] }) validates the graph. If submissions doesn't ship SubmissionAutoGradedEvent or marks it internal, boot fails with a helpful message.

externalEvents

For events that come from OTHER SERVICES (not other modules in the same app), use needs.externalEvents. Same shape; different validator:

ts
defineModule("competency", {
  needs: {
    externalEvents: [SubmissionAutoGradedEvent],   // from the lx service
  },
})

createApp requires a bus (@nwire/bus-* adapter) when externalEvents is non-empty. The framework auto-subscribes to the bus topic at boot.

See the Multi-service guide for the full pattern.

Studio-aware

FieldWhat Studio does
descriptionModule tooltip + detail panel
ownersTeam tag for who maintains the module
journey: [{ id, label }]Powers the EventStorm Process Flow journey picker

Folder convention

Studio + the scanner work best when modules follow this shape:

modules/submissions/
  src/
    submissions.module.ts            # this defineModule call
    context/
      submissions.events.ts           # defineEvent + eventFactory
      submission.types.ts             # branded ids, value objects
      submission.actor.ts             # defineActor
    handlers/
      submit-answer.action.ts         # defineAction
      submit-answer.handler.ts        # defineHandler (if separated)

    workflows/
      auto-grade.workflow.ts          # defineWorkflow (reactions, translators, sagas)
    projections/
      submissions-by-student.projection.ts
      submissions-by-student.query.ts
    external/
      notify-student.call.ts          # defineExternalCall
    routes/
      submissions.routes.ts           # defineRoute
  package.json                        # one workspace package per module
  tsconfig.json

See Concepts → Module for the rationale.

See also

MIT licensed.