Create a Domain
This guide walks through creating a new domain for the Backcap registry. We’ll use a hypothetical notifications domain as the running example. Follow the numbered checklist in order.
Before You Start
Section titled “Before You Start”A domain is a vertical slice of backend business logic structured in three layers: domain/, application/, and contracts/. Adapters (port implementations) are authored by you outside the domain directory. Read the Domains concept page and the Architecture page before proceeding.
Checklist
Section titled “Checklist”1. Define the Bounded Context
Section titled “1. Define the Bounded Context”Write a one-paragraph description of what the domain does and what it does not do. Be explicit about boundaries.
Example:
The
notificationsdomain manages the delivery of transactional messages to users. It defines theINotificationSenderport, aSendNotificationuse case, and typed results for delivery success and failure. It does not handle email provider configuration, template rendering, or user preference management.
2. Create the Directory Structure
Section titled “2. Create the Directory Structure”mkdir -p packages/registry/domains/notifications/domain/entitiesmkdir -p packages/registry/domains/notifications/domain/value-objectsmkdir -p packages/registry/domains/notifications/domain/errorsmkdir -p packages/registry/domains/notifications/domain/eventsmkdir -p packages/registry/domains/notifications/domain/__tests__mkdir -p packages/registry/domains/notifications/application/use-casesmkdir -p packages/registry/domains/notifications/application/portsmkdir -p packages/registry/domains/notifications/application/dtomkdir -p packages/registry/domains/notifications/application/__tests__/mocksmkdir -p packages/registry/domains/notifications/contractsmkdir -p packages/registry/domains/notifications/shared3. Copy the Result Monad
Section titled “3. Copy the Result Monad”Copy shared/result.ts from an existing domain. This file is duplicated intentionally so each domain is self-contained:
cp packages/registry/domains/auth/shared/result.ts \ packages/registry/domains/notifications/shared/result.ts4. Define Domain Errors
Section titled “4. Define Domain Errors”Create typed error classes for every expected failure condition. Each error extends Error and has a static factory method:
export class NotificationFailed extends Error { static create(reason: string): NotificationFailed { return new NotificationFailed(`Notification delivery failed: ${reason}`); }}Rules:
- One file per error class
- File name:
<kebab-name>.error.ts - Export name:
PascalCase - No external imports
5. Define Value Objects (if needed)
Section titled “5. Define Value Objects (if needed)”Value objects wrap and validate primitives. Use them when a primitive carries business meaning:
import { Result } from "../../shared/result.js";
const VALID_CHANNELS = ["email", "sms", "push"] as const;type Channel = (typeof VALID_CHANNELS)[number];
export class NotificationChannel { private constructor(readonly value: Channel) {}
static create(value: string): Result<NotificationChannel, Error> { if (!VALID_CHANNELS.includes(value as Channel)) { return Result.fail(new Error(`Invalid channel: ${value}`)); } return Result.ok(new NotificationChannel(value as Channel)); }}Rules:
- Private constructor — always use a static factory that returns
Result - Immutable — no setters
- No external imports
6. Define Domain Entities (if needed)
Section titled “6. Define Domain Entities (if needed)”Entities are domain objects with identity. Not every domain needs entities — some are purely service-oriented.
Rules:
- Private constructor
- Factory method returns
Result<Entity, DomainError> - All mutation methods return
Result<Entity, DomainError>(return a new instance) - No external imports
7. Define Domain Events
Section titled “7. Define Domain Events”Domain events represent something that happened. They are emitted by use cases and can be consumed by event handlers you wire up:
export class NotificationSent { readonly occurredAt: Date;
constructor( readonly notificationId: string, readonly userId: string, readonly channel: string, ) { this.occurredAt = new Date(); }}8. Write Domain Tests
Section titled “8. Write Domain Tests”Test all domain logic before moving to the application layer:
describe("NotificationChannel", () => { it("creates a valid channel", () => { const result = NotificationChannel.create("email"); expect(result.isOk()).toBe(true); expect(result.unwrap().value).toBe("email"); });
it("rejects an invalid channel", () => { const result = NotificationChannel.create("carrier-pigeon"); expect(result.isFail()).toBe(true); });});9. Define Port Interfaces
Section titled “9. Define Port Interfaces”Ports describe what the use cases need from external systems. One interface per external concern:
import type { SendNotificationDto } from "../dto/send-notification.dto.js";
export interface INotificationSender { send(dto: SendNotificationDto): Promise<void>;}Rules:
interfacekeyword (notabstract class)- Filename:
<kebab-name>.port.ts - Export name:
I<PascalName> - Import from
domain/only (for entity types if needed)
10. Define DTOs
Section titled “10. Define DTOs”DTOs are plain data shapes for use case inputs and outputs. No methods, no validation logic:
export interface SendNotificationDto { userId: string; channel: string; subject: string; body: string;}11. Implement Use Cases
Section titled “11. Implement Use Cases”One class per use case, one public execute() method:
import { Result } from "../../shared/result.js";import { NotificationFailed } from "../../domain/errors/notification-failed.error.js";import type { INotificationSender } from "../ports/notification-sender.port.js";import type { SendNotificationDto } from "../dto/send-notification.dto.js";
export class SendNotification { constructor(private readonly sender: INotificationSender) {}
async execute(dto: SendNotificationDto): Promise<Result<void, NotificationFailed>> { try { await this.sender.send(dto); return Result.ok(undefined); } catch (err) { return Result.fail(NotificationFailed.create((err as Error).message)); } }}Rules:
- Constructor receives port interfaces only (no concrete classes)
execute()returnsResult<T, E>— never throws for expected failures- Import from
domain/andapplication/only
12. Write Application Tests with Mocks
Section titled “12. Write Application Tests with Mocks”export class MockNotificationSender implements INotificationSender { sent: SendNotificationDto[] = [];
async send(dto: SendNotificationDto): Promise<void> { this.sent.push(dto); }}describe("SendNotification", () => { it("sends a notification successfully", async () => { const sender = new MockNotificationSender(); const useCase = new SendNotification(sender);
const result = await useCase.execute({ userId: "user-1", channel: "email", subject: "Hello", body: "Welcome!", });
expect(result.isOk()).toBe(true); expect(sender.sent).toHaveLength(1); });});13. Define the Public Contract
Section titled “13. Define the Public Contract”import type { Result } from "../shared/result.js";import type { SendNotificationDto } from "../application/dto/send-notification.dto.js";import type { NotificationFailed } from "../domain/errors/notification-failed.error.js";
export interface INotificationsService { send(dto: SendNotificationDto): Promise<Result<void, NotificationFailed>>;}14. Write the Factory Function
Section titled “14. Write the Factory Function”import type { INotificationSender } from "../application/ports/notification-sender.port.js";import { SendNotification } from "../application/use-cases/send-notification.use-case.js";import type { INotificationsService } from "./notifications.contract.js";
export type NotificationsServiceDeps = { notificationSender: INotificationSender;};
export function createNotificationsService( deps: NotificationsServiceDeps,): INotificationsService { const sendNotification = new SendNotification(deps.notificationSender); return { send: (dto) => sendNotification.execute(dto), };}15. Write the Barrel index.ts
Section titled “15. Write the Barrel index.ts”export type { INotificationsService } from "./notifications.contract.js";export { createNotificationsService } from "./notifications.factory.js";export type { NotificationsServiceDeps } from "./notifications.factory.js";This is the only barrel export in the domain. Do not create index.ts files in domain/ or application/.
16. Create the SKILL.md
Section titled “16. Create the SKILL.md”Write a skill file at packages/registry/skills/backcap-notifications/SKILL.md. See the Skills concept page for the required format.
17. Bundle for the Registry
Section titled “17. Bundle for the Registry”Build the registry to produce a JSON bundle (notifications.json) containing all source files as content strings. The CLI fetches this bundle when users run npx @backcap/cli add notifications.
pnpm --filter @backcap/registry buildThe bundle format matches the registryItemSchema in packages/shared/. The build step runs runQualityChecks() from quality-check.ts, which enforces the domain directory structure and naming conventions at build time.
Checklist Summary
Section titled “Checklist Summary”- Bounded context defined in writing
- Directory structure created
-
shared/result.tscopied - Domain errors defined and tested
- Value objects defined and tested (if applicable)
- Domain entities defined and tested (if applicable)
- Domain events defined
- Port interfaces defined
- DTOs defined
- Use cases implemented and tested with mocks
- Public contract interface defined
- Factory function written
-
contracts/index.tsbarrel created -
SKILL.mdwritten - Registry bundle produced