Skip to content

Module (Bounded Context)

A bounded context: a related set of actions, events, actors, projections, queries, reactions, and orchestrator primitives that belong together.

Shape

ts
import { defineModule } from "@nwire/forge";

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

  // Domain primitives
  actions: [submitAnswer, gradeSubmission, flagForReview, reviewSubmission],
  events: [
    AnswerSubmittedEvent,
    SubmissionAutoGradedEvent,
    SubmissionFlaggedForReviewEvent,
    SubmissionManuallyGradedEvent,
  ],
  actors: [Submission],
  projections: [SubmissionsByStudent],
  queries: [submissionsByStudent],
  reactions: [autoGradeReaction],
  handlers: [/* if any are registered separately from their action */],

  // Orchestrator primitives
  externalCalls: [notifyStudent, chargeStripe],
  inboundWebhooks: [stripeWebhook],
  outboxes: [submissionsOutbox],
  inboxes: [submissionsInbox],
  crons: [dailyReviewSummary],

  // HTTP routes
  routes: defineRoute({
    "POST /submissions": submitAnswer,
    "GET /submissions": submissionsByStudent,
    "POST /submissions/:id/flag-for-review": flagForReview,
  }),

  // Cross-module declared dependencies
  needs: {
    events: [],                        // public events from OTHER modules I subscribe to
    externalEvents: [],                // events from OTHER services (via bus)
    actions: [],                       // actions from OTHER modules I dispatch
  },
});

needs — explicit dep graph

createApp validates at boot that every needs.events / needs.actions is provided by some other module. Catches typos + missing modules before the first dispatch.

ts
defineModule("mastery", {
  needs: {
    events: [SubmissionAutoGradedEvent, SubmissionManuallyGradedEvent],
  },
  reactions: [updateOnGradedReaction, updateOnReviewedReaction],
});

If SubmissionAutoGradedEvent is visibility: 'internal', createApp throws — internal events can't be subscribed by other modules.

What modules SHOULD look like

  • ✅ One bounded context per module
  • ✅ Domain shapes live in context/
  • ✅ Actions + handlers live in handlers/
  • ✅ Reactions live in reactions/
  • ✅ Projections + queries live in projections/
  • ✅ External boundaries live in external/
  • ✅ HTTP routes in routes/

(See Architecture principles for the canonical folder shape.)

Reusable modules vs consumer modules

  • Consumer modules (in your app's repo): submissions, enrollments, mastery — your business
  • Reusable modules (in nwire/modules/): auth, billing, notifications — domain-agnostic, shipped by the framework

Studio-aware metadata

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

See also

MIT licensed.