agentsclimarketplace

Ts ddd domain service

Skill llodev/skills/skills/ts-ddd-domain-service

Collection of Agent Skills for Claude Code, Cursor, Codex, Windsurf, and any agent that speaks the open Skills spec. Ships via npm + plugin + skills add.

Install
npx -y skills add llodev/skills --skill ts-ddd-domain-service

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Create, review, or guide pure Domain Service implementation in a TypeScript + DDD monorepo. Use when the request involves `*.service.ts` files inside `apps/api/src/<bc>/domain/services/`, domain policies, pure calculations/rules across multiple entities/VOs, naming patterns (`*Policy`, `*Calculator`, `*Resolver`, `*Specification`), branching on closed-set enums (status / kind / role) from `@acme/celebrations-contracts`, or tests in `apps/api/test/<bc>/domain/services/`. Scope is the *pure* domain layer; for orchestrators that need a repository or other I/O (e.g. `SlugAllocator`), see the sibling `application/services/` section at the end.

SKILL.md

8.0 KB, as published. Nobody here has run it

TypeScript DDD Domain Service

MANDATORY — READ ENTIRE FILE: Before any implementation step, read references/domain-service-pattern.md completely. Do NOT load other DDD skills (entity, use-case, repository) unless explicitly requested.

This skill covers pure domain services that live in apps/api/src/<bc>/domain/services/. They never see a repository, NestJS, Firestore, HTTP, Zod, or any DTO. They take entities/VOs/primitives as arguments and return a Result or a plain domain value.

If the rule needs I/O (load by slug, retry against the database, call an external API), it is an application service, not a domain service — jump straight to the "When you need I/O" section below.


Before You Start

Before writing a single method, answer:

  • Ownership: does the rule use only one entity's own data? → put it as an entity method + cloneWith (see Celebration.publish), not a service.
  • Cross-aggregate?: does it combine 2+ entities/VOs without a clear owner? → Domain Service.
  • I/O needed?: does the rule require a repository, query, Firestore, or external call? → Application Service in application/services/ (DI + @Injectable), not Domain Service.
  • Already exists?: is the rule already captured in a VO (Slug, PaletteKey) or entity method (addSection, publish)? → do not duplicate.
  • Closed-set branch?: if you compare a status/kind/role against a literal string, stop — use the enum from @acme/celebrations-contracts (see "Branching on closed sets" in the reference).

Domain Service vs Use Case vs Application Service

For the Entity Method criteria (single-aggregate invariants — when the rule belongs inside *.entity.ts rather than in any service), see the ts-ddd-entity skill references. This skill owns the boundary between the three service-shaped homes below.

SituationWhere to put itWhy
Rule combines 2+ entities/VOs with no clear owner, no I/ODomain Service (domain/services/)No single entity owns the rule; pure → no DI needed
Rule is pure but only used in one flowInline in Use CaseYAGNI — extract to service only when reused
Rule needs I/O (repository, query, Firestore, HTTP)Use Case or Application ServiceDomain layer must stay pure
Multi-step I/O flow that doesn't fit UseCase<IN, OUT>Application Service (application/services/)SlugAllocator-style: needs DI, retries, repo access
Rule is a boolean policy over a collectionDomain Service (*Policy)Stateless check over multiple objects

Core Rules

  • File lives in apps/api/src/<bc>/domain/services/<name>.service.ts — never under application/, infra/, presentation/, or libs/contracts/**.
  • Each leaf folder has an index.ts barrel — re-export every new service from apps/api/src/<bc>/domain/services/index.ts.
  • Pure and deterministic: same input always produces same output, no Date.now()/Math.random() inside the rule (inject if needed).
  • Imports allowed: @acme/shared (Result, Entity, VOs), @acme/<bc>-contracts (enums + interfaces), other entities/VOs in the same BC. Nothing else.
  • No @nestjs/*, @Injectable, constructor injection, firebase-admin, Zod, DTOs, or framework decorators in this file.
  • Receives domain objects (entities, VOs, primitives) — returns Result<T> or a plain domain value.
  • Name by rule intent: *Policy, *Calculator, *Resolver, *Specification.
  • Default to a class with static methods for stateless calculations; instance methods only when the service genuinely holds configuration that varies per call site.
  • Compare closed-set fields against enum members (CelebrationStatusEnum.PUBLISHED), never raw strings. Iterate catalogs via Object.values(XxxEnum).
  • On failure use Result.fail("DOMAIN_ERROR_CODE") (UPPER_SNAKE). Aggregate multi-error validation with Result.combine([...]) or by pushing into an errors[] array — same pattern as Celebration.tryCreate.
  • Bubble up failures from inner VOs/entities via result.withFail instead of re-wrapping.

NEVER

  • NEVER inject a repository, query, or Firestore client into a Domain Service — I/O belongs in a Use Case or Application Service.
  • NEVER import @nestjs/*, firebase-admin, next, react, or any framework here — that contamination is what the layer exists to prevent.
  • NEVER duplicate logic already in an entity method or VO — if Celebration exposes publish(), the service calls it instead of re-implementing the transition.
  • NEVER put orchestration decisions (which repository to call next, which event to emit, retry loops) in a Domain Service — that is application concern.
  • NEVER return null/undefined/throw to signal failure — return Result.fail("CODE") so callers handle errors uniformly.
  • NEVER branch on a raw string literal for status/kind/role/palette — import the enum from @acme/celebrations-contracts (or the relevant BC contracts).
  • NEVER call the function execute here — execute is reserved for UseCase<IN, OUT>. Name the method after the rule (check, calculate, resolve, isSatisfiedBy).
  • NEVER call Date.now(), Math.random(), crypto.randomUUID(), performance.now(), new Date() (without an argument), or any other non-deterministic primitive inside a domain service — pure means deterministic given inputs. Pass a clock/random/id-generator as a parameter (or generate the value in the use case before calling the service). Why: non-determinism makes tests flaky, breaks property-based testing, and hides the real input set the rule depends on — the service silently couples to wall-clock time instead of taking it as data.

When you need I/O — use an Application Service

If your "service" needs a repository, retries against the database, or external calls, it is not a Domain Service. Put it in apps/api/src/<bc>/application/services/<name>.service.ts, mark it @Injectable(), inject ports via DI tokens, and still return Result. Canonical reference in this repo: apps/api/src/celebrations/application/services/slug-allocator.service.ts (allocates a unique Slug by retrying against CelebrationRepository). See the final section of references/domain-service-pattern.md for the contrast.

Verification

After writing or editing a service, run:

pnpm --filter api typecheck && pnpm --filter api test

(or make check-api). Test files live in apps/api/test/<bc>/domain/services/<name>.test.ts.

References

See references/domain-service-pattern.md for: file paths, canonical snippets (*Policy and *Calculator), the closed-set enum rule, test strategy, the implementation checklist, and the application-service contrast.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.