Ts ddd domain service
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.From its SKILL.md
npx -y skills add llodev/skills --skill ts-ddd-domain-serviceAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things 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.
- runs commandsInstructs the agent to run 2 commands, including `pnpm --filter api typecheck && pnpm --filter api test` and 1 more.
SKILL.md
8.0 KB, ~1.7k tokens by cl100k_base, 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(seeCelebration.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.tsrather than in any service), see thets-ddd-entityskill references. This skill owns the boundary between the three service-shaped homes below.
| Situation | Where to put it | Why |
|---|---|---|
| Rule combines 2+ entities/VOs with no clear owner, no I/O | Domain Service (domain/services/) | No single entity owns the rule; pure → no DI needed |
| Rule is pure but only used in one flow | Inline in Use Case | YAGNI — extract to service only when reused |
| Rule needs I/O (repository, query, Firestore, HTTP) | Use Case or Application Service | Domain 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 collection | Domain Service (*Policy) | Stateless check over multiple objects |
Core Rules
- File lives in
apps/api/src/<bc>/domain/services/<name>.service.ts— never underapplication/,infra/,presentation/, orlibs/contracts/**. - Each leaf folder has an
index.tsbarrel — re-export every new service fromapps/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
staticmethods 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 viaObject.values(XxxEnum). - On failure use
Result.fail("DOMAIN_ERROR_CODE")(UPPER_SNAKE). Aggregate multi-error validation withResult.combine([...])or by pushing into anerrors[]array — same pattern asCelebration.tryCreate. - Bubble up failures from inner VOs/entities via
result.withFailinstead 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
Celebrationexposespublish(), 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/throwto signal failure — returnResult.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
executehere —executeis reserved forUseCase<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.
What ships with it: 10 files
39.9 KB alongside SKILL.md, 3 of them executable
docs/
- i18n/README.es-ES.md5.7 KB
- i18n/README.pt-BR.md5.7 KB
examples/
- permission-policy.service.test.tsruns2.7 KB
- permission-policy.service.tsruns1.6 KB
- stock-calculator.service.tsruns1.6 KB
references/
- domain-service-pattern.md14.4 KB
- CHANGELOG.md1016 B
- LICENSE1.1 KB
- package.json798 B
- README.md5.3 KB
Gives 0 of the 12 instructions most test skills give in ~1.7k tokens
Counted across 1,201 of the 2,096 authors here whose files we hold, read 2026-09-06
- Write a failing test before writing codein 43 of 1201, across 36 files
- Run the full test suitein 36 of 1201, across 35 files
- Test only one variable per experimentin 34 of 1201, across 17 files
- Read product marketing context before asking questionsin 34 of 1201, across 14 files
- Mock external dependenciesin 34 of 1201, across 30 files
- Define primary, secondary, and guardrail metricsin 33 of 1201, across 16 files
- Pre-determine sample size before startingin 31 of 1201, across 14 files
- Test behavior rather than implementationin 31 of 1201, across 29 files
- Formulate a hypothesis before designing a testin 30 of 1201, across 13 files
- Document every test hypothesis, variant, and resultin 29 of 1201, across 11 files
- Use descriptive test function namesin 25 of 1201, across 21 files
- Commit to the methodology without stopping earlyin 24 of 1201, across 8 files
Said here and by no other author read
- Read the domain service pattern reference before implementation
- Place files in the domain services directory
- Re-export new services from the index file
- Ensure methods are pure and deterministic
- Use Result objects for all return values
- Name services by their rule intent
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.