defineModule
defineModule(name, manifest) declares a bounded context — a related set of actions, events, actors, projections, queries, workflows, orchestrator primitives, and HTTP routes.
Signature
function defineModule(name: string, manifest: ModuleManifest): ModuleDefinitionModuleManifest
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
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.
// 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:
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
| Field | What Studio does |
|---|---|
description | Module tooltip + detail panel |
owners | Team 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.jsonSee Concepts → Module for the rationale.
See also
- Concepts → Module
- defineApp — apps compose modules
- Multi-service guide — how needs.externalEvents resolves
- Architecture principles