From backend-services
Enforces hexagonal architecture with strict layer boundaries, dependency rules, and DTO isolation for production code in src/ only (NOT test code). Use when designing services, structuring production code, or working with domain/service/inbound/outbound layers.
npx claudepluginhub andercore-labs/claudes-kitchen --plugin backend-servicesThis skill uses the workspace's default tool permissions.
**SCOPE:** Production code under `src/` only. Test code has separate standards.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
SCOPE: Production code under src/ only. Test code has separate standards.
src/
├── domain/ → Pure business logic, entities, Result<T,E>
├── service/ → Orchestration, Task<T,E>, domain types only
├── inbound/ → HTTP/Events, DTOs → Domain → Service → DTOs
└── outbound/ → DB/APIs, Domain → External → Domain
Service design | layer structure | DTO boundaries | dependency rules | pipeline patterns
| Layer | Purpose | Imports | Forbidden | Returns |
|---|---|---|---|---|
| Domain | Pure business logic, entities only | Standard lib, true-myth (Result/Maybe) | @nestjs, I/O, logging, null/undefined, Task, async | Result<T,E>, Maybe |
| Service | Use case orchestration | Domain, outbound ports, DI, true-myth | Inbound, DTOs, null/undefined, schedulers | Task<T,E> |
| Inbound | Transport adapters (HTTP/events/cron) | @nestjs, domain, service, validation, schedulers | Outbound, DTOs in service calls | Promise (unwrapOrElse) |
| Outbound | Infrastructure adapters | Domain, external clients, true-myth | Inbound, service, exported adapter types, schedulers | Task<T,E> |
FORBIDDEN:
Module A → Module B → Module A (direct)
Module A → B → C → A (indirect)
Service ↔ Service (circular)
ALLOWED:
Service → Service (orchestration, composition)
Multiple → Same module (fan-in)
One → Multiple (fan-out)
DAG only (directed acyclic graph)
Fix pattern:
user.service ↔ order.service ✗ (circular)
→ user.service → order.service ✓ (orchestration)
→ Both import domain entity ✓ (shared logic)
Required:
| Pattern | Example |
|---|---|
| Constructor injection | constructor(@Inject('Repo') private repo: Repo) |
| All dependencies via DI | NO new ClassName() anywhere |
| type tokens | @Inject('UserRepository') |
Port creation (YAGNI):
| Implementations | Pattern |
|---|---|
| 1 implementation | Inject concrete: @Inject('PostgresRepo') private repo: PostgresRepo |
| 2+ implementations | Create port: type Repo = {...} then @Inject('Repo') |
Forbidden:
new Repository() → @Inject('Repository')
RepositoryFactory.create() → @Inject('Repository')
DatabaseSingleton.getInstance() → @Inject('Database')
module-level dependency → Constructor parameter
Port with 1 implementation → Direct concrete injection
interface → type
| Layer | DTOs | Domain Types | Rule |
|---|---|---|---|
| Inbound | Own request/response DTOs | Transform DTO ↔ Domain | NEVER pass DTOs to service |
| Service | FORBIDDEN | Domain types ONLY | NO DTO imports |
| Outbound | Own external API DTOs | Transform Domain ↔ External | NEVER export DTOs |
| Domain | FORBIDDEN | Entities only | NO DTO knowledge |
Flow:
HTTP → DTO → Validate → Domain → Service(Domain) → Domain → DTO → HTTP
External → DTO → Domain → Service → Domain → DTO → External
Violations:
service.create(dto: CreateUserDto) ✗
→ service.create(user: User) ✓
service returns UserDto ✗
→ service returns User, controller maps to DTO ✓
MUST be sync - NO async/Promise/Task
export const createResource = (params: Params): Result<Resource, Error> =>
validateParams(params)
.andThen(buildResource)
.map(Object.freeze)
export const filterActive = (resources: Resource[]): Resource[] =>
resources.filter(r => r.status === 'active')
MUST return Task<T,E>, ONLY domain types in signatures
createResource(userId: UserId, resource: Resource): Task<Resource, ServiceError> {
return fromResult(this.validateResource(resource))
.andThen(valid => this.checkDuplicate(valid))
.andThen(unique => this.outbound.save(unique))
}
Pipeline: Parse → Validate → Domain → Service → DTO → unwrapOrElse
@Post()
async create(@Body() dto: CreateDto): Promise<ResponseDto> {
return fromResult(this.validateDto(dto))
.andThen(this.toDomain)
.andThen(domain => this.service.create(domain))
.map(this.toResponseDto)
.mapRejected(this.mapError)
.toPromise()
.then(result => result.unwrapOrElse(error => Promise.reject(error)))
}
NO throw statements - use Promise.reject in unwrapOrElse
MUST return domain types, validate ALL external responses
fetchResource(id: ResourceId): Task<Resource, AdapterError> {
return fromPromise(
this.http.get(`/resources/${id.value}`),
this.mapHttpError
)
.andThen(this.validateResponse)
.andThen(this.parseData)
.map(this.toDomain)
}
| Layer | Rule |
|---|---|
| Domain | NO null/undefined - use Maybe |
| Service | NO null/undefined - use Maybe |
| Inbound | Check in DTOs → Convert to Maybe immediately |
| Outbound | Check external data → Convert to Maybe immediately |
Collections:
users: User[] not User[] | null
tags: string[] = [] not tags?: string[]
config: Maybe<Config> not config?: Config
| From ↓ To → | Domain | Service | Inbound | Outbound |
|---|---|---|---|---|
| Domain | ✓ | ✗ | ✗ | ✗ |
| Service | ✓ | ✓ (no circular) | ✗ | ✓ (ports) |
| Inbound | ✓ | ✓ | ✗ (same-layer) | ✗ |
| Outbound | ✓ | ✗ (types ok) | ✗ | ✗ (same-layer) |
Rules:
Domain → Nothing (pure)
Service → Domain + Service (orchestration) + Outbound ports
Inbound → Domain + Service (NO other inbound adapters)
Outbound → Domain only (NO other outbound adapters)
CRITICAL: Same-layer adapter imports FORBIDDEN
Inbound → Inbound ✗ (adapters isolated)
Outbound → Outbound ✗ (adapters isolated)
Timing/polling/scheduling = inbound adapter responsibility
| Layer | Rule |
|---|---|
| Inbound | ✓ Intervals, timers, cron, polling streams |
| Service | ✗ NO timing - respond to calls only |
| Outbound | ✗ NO timing - respond to calls only |
| Domain | ✗ NO timing - pure logic only |
Rationale:
External trigger → Inbound adapter
User request | Message | Cron | Poll → All inbound
Service → Orchestration (reactive, not proactive)
Examples:
// ✓ CORRECT: RxJS interval in inbound
export class ConfigPuller {
createUpdateStream(syncInterval: number): Observable<Config> {
return interval(syncInterval).pipe(
exhaustMap(() => this.service.fetchConfig())
)
}
}
// ✓ CORRECT: Timer in inbound
export class ReportScheduler {
constructor(private service: ReportService) {}
startSchedule() {
setInterval(() => this.service.generate(), 86400000)
}
}
// ✗ FORBIDDEN: Timer in service
export class ReportService {
generate() {
setInterval(() => {/*...*/}, 5000) // ✗ Service initiates
}
}
Framework-specific constraints:
NestJS: @Cron, @Schedule, @Interval → inbound only
RxJS: interval(), timer() → inbound only
Native: setInterval, setTimeout → inbound only (service/domain FORBIDDEN)
Required pattern:
export type AppConfig = Readonly<{
apiKey: string
baseUrl: string
}>
@Injectable()
export class ConfigService {
private readonly config: AppConfig
constructor() {
this.config = Object.freeze({
apiKey: this.requireEnv('API_KEY'),
baseUrl: this.requireEnv('BASE_URL')
})
}
get(): AppConfig { return this.config }
}
@Injectable()
export class UserService {
constructor(
@Inject('AppConfig') private readonly config: AppConfig
) {}
}
Forbidden:
process.env.API_KEY in service ✗
Unvalidated config ✗
Mutable config ✗
| Violation | Fix |
|---|---|
| Service ↔ Service (circular) | Make unidirectional OR extract to domain |
| Service uses DTOs | Use domain types only |
| Domain has async | Domain MUST be sync Result<T,E> |
Direct new Class() | @Inject('Class') via constructor |
| Port with 1 implementation | Inject concrete class directly |
| interface keyword | Use type |
| DTO passed to service | Transform to domain first |
| Exported adapter types | Keep internal, return domain |
| null in domain/service | Use Maybe |
| process.env in service | Inject typed config |
| Inbound → Inbound import | Extract shared logic to service or domain |
| Outbound → Outbound import | Extract shared logic to domain |
| Service/outbound initiates timing | Move timing to inbound adapter |
| Domain has timers | Remove - domain must be pure |
MANDATORY: Run after design or when reviewing code.
| Phase | Action |
|---|---|
| 1. Scan | Identify files by layer (domain/service/inbound/outbound) |
| 2. Validate | Run all checks → gather violations |
| 3. Report | ✓ ALL pass → Done | ✗ ANY fail → List with evidence |
| 4. Fix | Violations → Fix → Re-validate |
| 5. Store Metrics | After ALL validation passes, call mcp__agent-orchestrator__store-skill-metrics |
1. Layer Dependencies (AUTHORITATIVE)
pnpm run lint:deps # Tool provides authoritative layer boundary violations
| Violation | Severity | Fix |
|---|---|---|
| Domain → Service/Inbound/Outbound | CRITICAL | Remove import |
| Service → Inbound | CRITICAL | Remove import |
| Outbound → Service/Inbound | CRITICAL | Remove import |
| Service ↔ Service (circular) | CRITICAL | Make unidirectional |
| Service → Service (orchestration) | ALLOWED | Composition/orchestration OK |
2. SDK Wrapping
| Pattern | Severity | Fix |
|---|---|---|
| Service/Domain imports mongodb, kafkajs, ioredis | CRITICAL | Create adapter in outbound/ |
| Service uses MongoDB.Collection, Kafka.Producer types | CRITICAL | Adapter exposes domain types |
| Controller imports SDK | CRITICAL | Use service → adapter |
3. Adapter Quality
| Pattern | Severity | Fix |
|---|---|---|
| Adapter has validation, calc, or business rules | CRITICAL | Move to domain/service |
| Adapter calls adapter directly | ERROR | Inject via constructor |
| Exposes MongoServerError, KafkaJSError to service | ERROR | Translate to domain errors |
| Returns MongoDB Document, Kafka Record | CRITICAL | Transform to domain entities |
4. Data Boundaries
| Pattern | Severity | Fix |
|---|---|---|
| Service returns/accepts MongoDB Document | CRITICAL | Use domain entity |
| Service uses ConsumerRecord, ProducerRecord types | CRITICAL | Use domain event types |
| Domain accepts HTTP Request, controller DTO | CRITICAL | Use primitives OR domain VOs |
| Domain entity uses MongoDB ObjectId | ERROR | Use string OR domain UUID VO |
| Service references Kafka headers, Redis reply | ERROR | Adapt in adapter |
5. Event Architecture
| Pattern | Severity | Fix |
|---|---|---|
| Kafka schema without version field | CRITICAL | Add version: string |
| Consumer has validation, calc, logic | CRITICAL | Delegate to service |
| Domain event imports kafkajs types | ERROR | Keep pure, adapter handles Kafka |
| Domain knows Avro, Protobuf format | ERROR | Adapter handles serialization |
6. Repository Pattern
| Pattern | Severity | Fix |
|---|---|---|
| Repository.findUser() returns MongoDB Document | CRITICAL | Map to domain entity |
| Service builds MongoDB query objects | ERROR | Repository exposes domain methods |
| Repository has calc/validation | ERROR | Return raw, domain/service handles |
| Service constructs MongoDB pipeline | ERROR | Encapsulate in repository |
7. Dependency Injection
| Pattern | Severity | Fix |
|---|---|---|
| Service with 'new MongoAdapter()' | CRITICAL | Constructor injection |
| Service → Adapter → Service cycle | CRITICAL | Make unidirectional |
| Calls getInstance() | ERROR | Inject via constructor |
| Adapter not injected | ERROR | Declare in constructor for DI |
8. Infrastructure Coupling
| Pattern | Severity | Fix |
|---|---|---|
| Domain as Mongoose Schema | CRITICAL | Separate plain class from schema (adapter) |
| Service has Redis key constants | ERROR | Move to Redis adapter |
| Service decides Redis TTL | ERROR | Adapter OR config |
| Service has Redis Lua scripts | ERROR | Adapter encapsulates |
| Domain coupled to Kafka Avro, Redis MessagePack | ERROR | Adapter handles |
9. Controller Architecture
| Pattern | Severity | Fix |
|---|---|---|
| Controller imports/calls controller | CRITICAL | Only call service |
| Controller has validation, calc, rules, transform | CRITICAL | Move to service |
| Calls 2+ business services | CRITICAL | Create orchestration service |
| Complex workflow across services | CRITICAL | Create orchestration service |
| Calls business + cross-cutting (auth/config/monitoring/logging/rate-limit) | ALLOWED | Guards and observability OK |
| Manipulates domain entities, applies rules | CRITICAL | Delegate to service |
Cross-cutting exceptions:
Auth, config, monitoring, logging, rate-limit → Always allowed alongside business service
Guards (JwtAuthGuard, RoleGuard, etc.) → Always allowed to inject services
10. Same-Layer Adapter Isolation (CRITICAL)
| Pattern | Severity | Fix |
|---|---|---|
| Inbound adapter imports another inbound adapter | CRITICAL | Share logic via service or domain |
| Outbound adapter imports another outbound adapter | CRITICAL | Share logic via domain or create orchestration service |
| Adapter directly instantiates another adapter | CRITICAL | Use dependency injection via service layer |
| Shared logic between adapters | CRITICAL | Extract to domain (pure logic) OR service (orchestration) |
11. Scheduling Constraints (CRITICAL)
| Pattern | Severity | Fix |
|---|---|---|
| Service/outbound has timing mechanisms | CRITICAL | Move to inbound adapter |
| Domain has setInterval, setTimeout, timers | CRITICAL | Remove - domain must be pure |
| Service initiates intervals, polling, cron | CRITICAL | Move to inbound adapter |
| Outbound initiates intervals, polling, cron | CRITICAL | Move to inbound adapter |
Detection:
setInterval, setTimeout → Service/domain FORBIDDEN
interval(), timer() (RxJS) → Service/outbound FORBIDDEN
Framework-specific → See framework skill (NestJS, etc.)
{
"discipline": "architecture",
"timestamp": "ISO8601",
"layer_violations": [
{"file":"src/domain/user.ts","line":15,"severity":"critical","rule":"layer_boundary","violation":"Domain imports from service","fix":"Remove import"}
],
"sdk_wrapping": [
{"file":"src/service/user.service.ts","line":12,"severity":"critical","rule":"sdk_direct","violation":"Imports 'mongodb' without adapter","fix":"Create MongoAdapter in outbound/"}
],
"adapter_quality": [],
"data_boundaries": [],
"event_architecture": [],
"repository_pattern": [],
"dependency_injection": [],
"infrastructure_coupling": [],
"controller_architecture": [],
"same_layer_adapter_isolation": [],
"scheduling_constraints": [],
"summary": {"critical":2,"errors":0,"warnings":0}
}
Review code/file structure → Gather evidence → Cite file:line
NOT: Re-execute OR invent violations
Re-validation required after fixes. Repeat until ALL checks pass.
See examples.md for complete service implementations