agentleFS
gabriellstgabriellst/codm/.claude/skills/bounded-context/SKILL.md

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
  1. Very specific:

    Every bounded context is bootstrapped via BoundedContext.create() — a shared abstraction in @codm/core-typescript that encapsulates child …

    line 16
  2. Very specific:

    Read docs/BACKEND.md — Bounded Contexts section + folder shape

    line 36
  3. Distinctive:

    Ensure you're in the monorepo root directory

    line 37
  4. Very specific:

    Not already existing in packages/api/typescript/src/

    line 46
  5. Distinctive:

    Run the CLI command:

    line 50
  6. Very specific:

    Every bounded context must have a flat registry.ts file (not a folder) exporting INSTANCE_REGISTRY:

    line 131
  7. Very specific:

    After creating the context, you MUST add it to the CONTEXTS manifest (packages/api/typescript/src/shared/contexts.ts) and wire its router …

    line 159
  8. Very specific:

    Add the new context's enums import to packages/api/typescript/src/shared/index.ts:

    line 181
  9. Very specific:

    Add error mappings to the GlobalErrorMapper record.

    line 219
  10. Distinctive:

    After creating a context, typically you will:

    line 286
  11. Very specific:

    Use /errors to define all error types

    line 287
  12. Very specific:

    Use /enum to define domain enums

    line 288
  13. Very specific:

    Use /controller to create HTTP endpoints

    line 289
  14. Very specific:

    Use /sdk to generate the SDK

    line 290
  15. Very specific:

    When you stand up an entire new domain — this is the shape, in order.

    line 294
  16. Very specific:

    If you discover the new context exceeds ~8 entities, stop and invoke /ddd-modeling to evaluate splitting it.

    line 329

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

BEFORE IMPLEMENTING: Open registry.yaml and read:

  1. patterns — all when: always are mandatory; evaluate each conditional (when: <condition>) before coding
  2. bad_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 ui context

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() + exports

Which folders get an index.ts is DECLARED, not decided per context — the table is scripts/lib/context-layers.ts, the gate is scripts/context-barrels.test.ts, and bun cli reads 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.router

BoundedContext.create() handles:

  • Child container creation (inherits from root)
  • Auto-tracing
  • Mediator resolution + handler registration
  • Job scheduling (if jobs is provided)
  • Custom setup callback (if setup is 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 | InterfaceErrors

Step 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

OptionRequiredDescription
nameyesContext name (used as URL prefix: /<name>)
rootnotrue for the shared context only — registers on root container
controllersyesNamespace import of controllers (import * as controllers from './controllers')
middlewaresnoDefault import of middlewares (import middlewares from './middlewares')
skipMiddlewaresnoMiddlewares to skip for this context
internalHandlersnoNamespace import of internal event handlers
externalHandlersnoNamespace import of cross-context event handlers
registrynoInstanceRegistry with mock/integration/real DI entries
jobsnoArray of { handler, repeat } for scheduled jobs
setupnoAsync 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: always patterns 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_practices violations (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 GlobalErrorMapper

Next Steps

After creating a context, typically you will:

  1. Use /errors to define all error types
  2. Use /enum to define domain enums
  3. Use /controller to create HTTP endpoints
  4. Use /sdk to 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-context billing (folder + registry.ts + entries in CONTEXTS, ROUTERS, and CONTEXT_REGISTRIES)

First slice (issue invoice — pick the smallest end-to-end behavior)

  • entity Invoice (id, customer, items, status, dueDate)
  • entity Payment (separate aggregate)
  • value-object Money (currency + amount)
  • value-object LineItem (description + amount + quantity)
  • enum InvoiceStatus = DRAFT | ISSUED | PAID | OVERDUE | CANCELED
  • enum PaymentMethod = PIX | CARD | BANK_SLIP
  • errors entry — InvoiceNotFound, InvoiceAlreadyPaid, InvalidAmount (single typed-union file per /errors skill)
  • db-modelling + migration for both tables
  • repository DrizzleInvoiceRepository, DrizzlePaymentRepository
  • usecase IssueInvoice, RecordPayment, CancelInvoice
  • controller POST /invoices, POST /invoices/:id/payments, PATCH /invoices/:id/cancel
  • schema per input/output
  • event domain InvoiceIssued, PaymentRecorded, InvoiceOverdue
  • handler InvoiceOverdueReminderHandler (sends reminder)
  • Integration events when other contexts react: billing.invoice.paid → patient updates balance.
  • Frontend
    • route /billing/invoices, /billing/invoices/:id
    • component InvoiceList, InvoiceDetail
    • form IssueInvoiceForm, RecordPaymentForm
    • query ListInvoicesQuery, 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 shape
  • packages/api/typescript/src/shared/types/BoundedContext.ts — BoundedContext class

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.