bounded-context
Skill4 starsChanged 2 months agoMITView on GitHub
Create a new bounded context (domain module) with full folder structure. Use when starting a new feature domain like 'product', 'order', 'user'. Use this skill whenever adding a new domain area that needs its own entities, use cases, controllers, and DI registration.
We split this skill into the rules it gives an agent
16 rules. About 0 are common, 3 are distinctive, and 13 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
13
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 16
Every bounded context is bootstrapped via BoundedContext.create() — a shared abstraction in @codm/core-typescript that encapsulates child …
- Very specific: line 36
Read docs/BACKEND.md — Bounded Contexts section + folder shape
- Distinctive: line 37
Ensure you're in the monorepo root directory
- Very specific: line 46
Not already existing in packages/api/typescript/src/
- Distinctive: line 50
Run the CLI command:
- Very specific: line 131
Every bounded context must have a flat registry.ts file (not a folder) exporting INSTANCE_REGISTRY:
- Very specific: line 159
After creating the context, you MUST add it to the CONTEXTS manifest (packages/api/typescript/src/shared/contexts.ts) and wire its router …
- Very specific: line 181
Add the new context's enums import to packages/api/typescript/src/shared/index.ts:
- Very specific: line 219
Add error mappings to the GlobalErrorMapper record.
- Distinctive: line 286
After creating a context, typically you will:
- Very specific: line 287
Use /errors to define all error types
- Very specific: line 288
Use /enum to define domain enums
- Very specific: line 289
Use /controller to create HTTP endpoints
- Very specific: line 290
Use /sdk to generate the SDK
- Very specific: line 294
When you stand up an entire new domain — this is the shape, in order.
- Very specific: line 329
If you discover the new context exceeds ~8 entities, stop and invoke /ddd-modeling to evaluate splitting it.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
BEFORE IMPLEMENTING: Open
registry.yamland read:
patterns— allwhen: alwaysare mandatory; evaluate each conditional (when: <condition>) before codingbad_practices— keep these violations in mind throughout implementation
Create Bounded Context
Creates a new domain context with the complete folder structure following DDD patterns.
Architecture
Every bounded context is bootstrapped via BoundedContext.create() — a shared abstraction in @codm/core-typescript that encapsulates child container creation, tracing, mediator wiring, job scheduling, and router instantiation.
The shared context (packages/api/typescript/src/shared/index.ts) is special: it uses root: true to register on the root container and passes ALL_REGISTRIES (shared services + all repository bindings). Other contexts create child containers that inherit from root.
Registry pattern: Each context has a flat registry.ts file exporting INSTANCE_REGISTRY built with expandBindings([...]) — one declaration per token, envs as columns. The shared registry (packages/api/typescript/src/shared/registry.ts) maps every context registry into CONTEXT_REGISTRIES (satisfies Record<ContextModule, InstanceRegistry>, compile-checked against the CONTEXTS manifest) and mechanically merges them into ALL_REGISTRIES. TestBed imports ALL_REGISTRIES directly for test DI.
When to Use This Skill
- Starting a new business domain (e.g., "billing", "scheduling")
- A group of related entities and operations that share a common language
- Existing contexts are becoming too large or handling unrelated concerns
When NOT to Use This Skill
- Adding features to an existing domain — add to the existing context instead
- Creating utility or helper code — use
shared/ - UI-specific read queries — use the
uicontext
Prerequisites
- Read
docs/BACKEND.md— Bounded Contexts section + folder shape - Ensure you're in the monorepo root directory
Process
Step 1: Validate Context Name
The context name should be:
- Lowercase, singular (e.g.,
product,order,user) - A business domain concept
- Not already existing in
packages/api/typescript/src/
Step 2: Create Context Structure [CTX-01, CTX-05]
Run the CLI command:
bun cli context <name>This creates the following structure:
packages/api/typescript/src/<context>/
├── controllers/ # HTTP controllers (endpoints)
│ └── index.ts # Barrel — STRUCTURAL: <ctx>/index.ts namespace-imports it
├── entities/ # Domain entities
│ # NO index.ts — import the module (`../entities/Thread`)
├── enums/ # Domain enums (status, type, category)
│ └── index.ts # Barrel — STRUCTURAL: shared/index.ts spreads it into registerEnums
├── errors/ # Error types (DomainErrors, ApplicationErrors, Errors)
│ └── index.ts # NOT a barrel — declares the unions and calls registerErrorCodes
├── events/ # Domain events
│ # NO index.ts — a handler cites ONE event by name
├── handlers/ # Event handlers
│ ├── internal.ts # Internal context handlers (named barrel, namespace-imported)
│ └── external.ts # Cross-context handlers (idem)
├── middlewares/ # HTTP middlewares
│ └── index.ts # Barrel — settled convention (61 imports through it, 3 around)
├── objects/ # Value objects and enums
│ └── index.ts # Barrel — STRUCTURAL in shared/ (registerSchemas spreads it)
├── repositories/ # Repository interfaces + implementations
│ # NO index.ts
├── registry.ts # DI bindings for mock/integration/real (flat file, not folder)
├── schemas/ # (optional) Zod schemas for shared domain validation
│ └── index.ts # Barrel — settled convention (29 through, 8 around)
├── services/ # Domain/Application services
│ # NO index.ts
├── usecases/ # Application use cases
│ # NO index.ts — import `@ui/usecases/GetHomeDashboard`
└── index.ts # BoundedContext.create() + exportsWhich folders get an
index.tsis DECLARED, not decided per context — the table isscripts/lib/context-layers.ts, the gate isscripts/context-barrels.test.ts, andbun clireads the same table so it cannot scaffold a barrel the rule forbids. Five layers have one (controllers,enums,objects,schemas,middlewares); the rest do not, and their modules are imported directly. See CTX-05 for the measurement behind the line. A NEW layer is a decision: add it to the table, with its why, in the same change that creates it.
Step 3: Create Context Index with BoundedContext.create() [CTX-02, BC-P01]
The context index.ts uses BoundedContext.create() to bootstrap the context declaratively:
// packages/api/typescript/src/<context>/index.ts
import * as controllers from './controllers'
import * as internalHandlers from './handlers/internal'
import * as externalHandlers from './handlers/external'
import middlewares from './middlewares'
import { BoundedContext } from '@codm/core-typescript'
const ctx = await BoundedContext.create({
name: '<context>',
controllers,
middlewares,
internalHandlers,
externalHandlers,
})
export const container = ctx.container
export default ctx.routerBoundedContext.create() handles:
- Child container creation (inherits from root)
- Auto-tracing
- Mediator resolution + handler registration
- Job scheduling (if
jobsis provided) - Custom setup callback (if
setupis provided) - Router instantiation
Step 4: Create the Context Registry [CTX-06, CTX-07]
Every bounded context must have a flat registry.ts file (not a folder) exporting INSTANCE_REGISTRY:
// packages/api/typescript/src/<context>/registry.ts
import { type InstanceRegistry, expandBindings } from '@codm/core-typescript'
import { ExampleRepository, MockExampleRepository, DrizzleExampleRepository } from './repositories'
// One declaration per token — `integration` omitted mirrors `real`; `null` = declared absence.
export const INSTANCE_REGISTRY: InstanceRegistry = expandBindings([
{ token: ExampleRepository, mock: MockExampleRepository, real: DrizzleExampleRepository },
])Then add the context to CONTEXT_REGISTRIES in the shared registry (packages/api/typescript/src/shared/registry.ts). The map is satisfies-checked against the CONTEXTS manifest, and the mechanical merge derives ALL_REGISTRIES — never hand-spread env arrays:
import { INSTANCE_REGISTRY as newContextRegistry } from '@<context>/registry'
const CONTEXT_REGISTRIES = {
shared: CORE_REGISTRY,
// ...existing contexts...
<context>: newContextRegistry,
} satisfies Record<ContextModule, InstanceRegistry>
// ALL_REGISTRIES is derived mechanically from CONTEXT_REGISTRIES — do not edit it.Step 5: Declare the Context + Register Router (CRITICAL!) [CTX-03, BC-P12]
After creating the context, you MUST add it to the CONTEXTS manifest (packages/api/typescript/src/shared/contexts.ts) and wire its router into the ROUTERS map in packages/api/typescript/src/routers.ts. The map is checked with satisfies Record<ContextModule, Router> — a declared context without a wired router is a compile error, not a silent gap:
// packages/api/typescript/src/shared/contexts.ts
export const CONTEXTS = {
// ...existing contexts...
<context>: { runtime: '<context>', pgSchema: '<context>' },
} as const satisfies Record<string, ContextDecl>
// packages/api/typescript/src/routers.ts
import NewContextRouter from '@<context>/index'
const ROUTERS = {
// ...existing contexts...
<context>: NewContextRouter,
} satisfies Record<ContextModule, Router>Both src/index.ts (server boot) and scripts/emit-openapi.ts consume ALL_ROUTERS from routers.ts — no other file needs touching.
Step 6: Register Enums in shared/index.ts [BC-P10]
Add the new context's enums import to packages/api/typescript/src/shared/index.ts:
import * as newContextEnums from '@<context>/enums'
openapi.registerEnums({ ...enums, ..., ...newContextEnums })Step 7: Initialize Errors [BC-P06]
Edit <context>/errors/index.ts to add initial error types and the Errors union:
import type { BaseDomainErrors, BaseApplicationErrors, BaseInterfaceErrors, BaseInfrastructureErrors } from '@codm/core-typescript'
export type <Context>DomainErrors = '<CONTEXT>_NOT_FOUND'
export type DomainErrors = BaseDomainErrors | <Context>DomainErrors
export type <Context>ApplicationErrors = never
export type ApplicationErrors = BaseApplicationErrors | <Context>ApplicationErrors
export type <Context>InterfaceErrors = never
export type InterfaceErrors = BaseInterfaceErrors | <Context>InterfaceErrors
export type <Context>InfrastructureErrors = never
export type InfrastructureErrors = BaseInfrastructureErrors | <Context>InfrastructureErrors
export type Errors = ApplicationErrors | DomainErrors | InfrastructureErrors | InterfaceErrorsStep 8: Register Errors in GlobalErrorMapper
Edit packages/api/typescript/src/shared/utils/GlobalErrorMapper.ts — import from the errors barrel:
import type { Errors as NewContextErrors } from '@<context>/errors'Add error mappings to the GlobalErrorMapper record.
BoundedContext Options Reference
| Option | Required | Description |
name | yes | Context name (used as URL prefix: /<name>) |
root | no | true for the shared context only — registers on root container |
controllers | yes | Namespace import of controllers (import * as controllers from './controllers') |
middlewares | no | Default import of middlewares (import middlewares from './middlewares') |
skipMiddlewares | no | Middlewares to skip for this context |
internalHandlers | no | Namespace import of internal event handlers |
externalHandlers | no | Namespace import of cross-context event handlers |
registry | no | InstanceRegistry with mock/integration/real DI entries |
jobs | no | Array of { handler, repeat } for scheduled jobs |
setup | no | Async callback for custom initialization |
Jobs
For recurring background work, use the jobs option:
import { ChatLoopHandler } from './handlers/ChatLoopHandler'
const ctx = await BoundedContext.create({
name: 'agent',
controllers,
middlewares,
internalHandlers,
externalHandlers,
jobs: [{ handler: ChatLoopHandler, repeat: { every: 30_000 } }],
})Supports { every: milliseconds } for intervals or { pattern: '* * * * *' } for cron expressions.
Checklist
- All
when: alwayspatterns present (CTX-01 through CTX-07, BC-P01, BC-P06, BC-P12 — verify against registry.yaml) - Each conditional pattern evaluated (CTX-C01, CTX-C02, BC-P03 through BC-P05, BC-P10, BC-P11 — check which apply)
- No
bad_practicesviolations (bp-01, bp-02 — verify against registry.yaml)
Example
Creating a product context:
# 1. Create context
bun cli context product
# 2. Edit product/index.ts — BoundedContext.create({ name: 'product', ... })
# 3. Create product/registry.ts with expandBindings([{ token, mock, real }]) declarations
# 4. Add product registry to CONTEXT_REGISTRIES in packages/api/typescript/src/shared/registry.ts
# 5. Declare product in CONTEXTS (src/shared/contexts.ts) + wire router in ROUTERS (src/routers.ts)
# 6. In packages/api/typescript/src/shared/index.ts, add enum registration
# 7. Edit product/errors/index.ts with initial errors + Errors type
# 8. Register errors in GlobalErrorMapperNext Steps
After creating a context, typically you will:
- Use
/errorsto define all error types - Use
/enumto define domain enums - Use
/controllerto create HTTP endpoints - Use
/sdkto generate the SDK
Composition Pattern — Greenfield context (full feature shape)
When you stand up an entire new domain — this is the shape, in order. Example: adding billing capability (billing) — issue invoices, record payments, send reminders.
Per-context one-time work
bounded-contextbilling(folder +registry.ts+ entries inCONTEXTS,ROUTERS, andCONTEXT_REGISTRIES)
First slice (issue invoice — pick the smallest end-to-end behavior)
entityInvoice(id, customer, items, status, dueDate)entityPayment(separate aggregate)value-objectMoney(currency + amount)value-objectLineItem(description + amount + quantity)enumInvoiceStatus = DRAFT | ISSUED | PAID | OVERDUE | CANCELEDenumPaymentMethod = PIX | CARD | BANK_SLIPerrorsentry —InvoiceNotFound,InvoiceAlreadyPaid,InvalidAmount(single typed-union file per/errorsskill)db-modelling+migrationfor both tablesrepositoryDrizzleInvoiceRepository,DrizzlePaymentRepositoryusecaseIssueInvoice,RecordPayment,CancelInvoicecontrollerPOST /invoices,POST /invoices/:id/payments,PATCH /invoices/:id/cancelschemaper input/outputeventdomainInvoiceIssued,PaymentRecorded,InvoiceOverduehandlerInvoiceOverdueReminderHandler(sends reminder)- Integration events when other contexts react:
billing.invoice.paid→ patient updates balance. - Frontend
route/billing/invoices,/billing/invoices/:idcomponentInvoiceList,InvoiceDetailformIssueInvoiceForm,RecordPaymentFormqueryListInvoicesQuery,GetInvoiceDetailQuery
Typical wave layout for a context this size:
- W1: entity + value-object + enum + errors (parallel)
- W2: db-modelling + migration
- W3: repository
- W4: usecase + controller + schema (parallel)
- W5: SDK contract lock
- W6: frontend (parallel)
If you discover the new context exceeds ~8 entities, stop and invoke /ddd-modeling to evaluate splitting it.
References
docs/BACKEND.md— Bounded Contexts section + folder shapepackages/api/typescript/src/shared/types/BoundedContext.ts— BoundedContext class
In this repository45 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- agentSkill
- clean-branchSkill
- commitSkill
- componentSkill
- controllerSkill
- db-modellingSkill
- ddd-modelingSkill
- ddd-specSkill
- design-systemSkill
- desktop-shellSkill
- e2eSkill
- entitySkill
- enumSkill
- errorsSkill
- eventSkill
- formSkill
- grill-with-docsSkill
- handlerSkill
- handoffSkill
- middlewareSkill
- migrateSkill
- prdSkill
- primitiveSkill
- projectionSkill
- projectorSkill
- querySkill
- repositorySkill
- reviewSkill
- routeSkill
- saas-researchSkill
- schedulerSkill
- schemaSkill
- sdkSkill
- serviceSkill
- spec-reviewSkill
- storeSkill
- storybookSkill
- task-breakdownSkill
- testSkill
- trace-analysisSkill
- ui-compositionSkill
- usecaseSkill
- user-storiesSkill
- value-objectSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

