بنقرة واحدة
hexagonal-architecture
Use when designing or reviewing system architecture. Hexagonal + DDD + CQRS patterns.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Use when designing or reviewing system architecture. Hexagonal + DDD + CQRS patterns.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Use when making git commits. Conventional commit format and rules.
Use when running security review on a diff/PR/file. Research-vs-Reporting discipline, three-tier confidence gate, attacker-controllable taxonomy.
Use when invoking plannotator for approval review of generated .tff-cc milestone artifacts (plan, verification, spec).
Use when starting design or discovery work. MUST use before any creative work.
Use when the user wants to report a bug, file an issue, or send feedback about tff-cc. Triggers on phrases like 'report issue', 'file a bug', 'tff-cc broke', 'something is wrong with tff-cc', 'open an issue', '/report-issue'.
Use when creating, refining, or composing skills. Evidence-driven pattern analysis.
| name | hexagonal-architecture |
| description | Use when designing or reviewing system architecture. Hexagonal + DDD + CQRS patterns. |
Reference implementations:
Presentation → Infrastructure → Application → Domain
(inward-only dependencies)
∀ layer L: imports(L) ⊆ inner_layers(L) — NO EXCEPTIONS
| Layer | Depends On | Never Depends On |
|---|---|---|
| Domain | nothing (pure) | App, Infra, Presentation |
| Application | Domain | Infra, Presentation |
| Infrastructure | Domain, Application | Presentation |
| Presentation | Application | Domain directly, Infra directly |
Dependency arrow always points inward. Outer layers know about inner layers; inner layers oblivious to outside world.
Innermost ring. Pure business logic, zero framework dependencies.
Allowed imports: zod, node:crypto, Result type — NOTHING else.
Result<Entity, DomainError>import { z } from 'zod';
import { Result, ok, err } from '@/shared/result';
const UserPropsSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(255),
createdAt: z.date(),
});
type UserProps = z.infer<typeof UserPropsSchema>;
export class User {
private constructor(private readonly props: UserProps) {}
static create(input: { email: string; name: string }): Result<User, DomainError> {
const parsed = UserPropsSchema.safeParse({
id: crypto.randomUUID(),
email: input.email,
name: input.name,
createdAt: new Date(),
});
if (!parsed.success) return err(DomainError.validation(parsed.error));
return ok(new User(parsed.data));
}
static reconstitute(props: UserProps): User {
return new User(props);
}
get id(): string { return this.props.id; }
get email(): string { return this.props.email; }
get name(): string { return this.props.name; }
rename(newName: string): Result<void, DomainError> {
const parsed = z.string().min(1).max(255).safeParse(newName);
if (!parsed.success) return err(DomainError.validation(parsed.error));
(this.props as any).name = parsed.data;
return ok(undefined);
}
}
readonly)const EmailSchema = z.string().email();
export class Email {
private constructor(readonly value: string) {}
static create(raw: string): Result<Email, DomainError> {
const parsed = EmailSchema.safeParse(raw);
if (!parsed.success) return err(DomainError.validation(parsed.error));
return ok(new Email(parsed.data));
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
UserCreatedEvent, OrderPlacedEvent{entity}-{action}.event.tsexport class UserCreatedEvent {
readonly occurredAt = new Date();
constructor(
readonly userId: string,
readonly email: string,
) {}
}
{aggregate}.repository.tsResult<T, DomainError> for fallible ops// domain/ports/user.repository.ts
export interface UserRepository {
findById(id: string): Promise<Result<User | null, DomainError>>;
save(user: User): Promise<Result<void, DomainError>>;
delete(id: string): Promise<Result<void, DomainError>>;
}
Orchestrates domain objects. One service per use case. Depends on domain only.
Result<void, AppError> ∨ Result<Id, AppError>{action}-{entity}.command.ts// application/commands/create-user.command.ts
export class CreateUserCommand {
constructor(
readonly email: string,
readonly name: string,
) {}
}
// application/commands/create-user.handler.ts
export class CreateUserHandler {
constructor(private readonly userRepo: UserRepository) {}
async execute(cmd: CreateUserCommand): Promise<Result<string, AppError>> {
const userResult = User.create({ email: cmd.email, name: cmd.name });
if (!userResult.ok) return err(AppError.fromDomain(userResult.error));
const saveResult = await this.userRepo.save(userResult.value);
if (!saveResult.ok) return err(AppError.fromDomain(saveResult.error));
return ok(userResult.value.id);
}
}
{action}-{entity}.query.ts// application/queries/get-user.query.ts
export class GetUserQuery {
constructor(readonly userId: string) {}
}
// application/queries/get-user.handler.ts
export class GetUserHandler {
constructor(private readonly userRepo: UserRepository) {}
async execute(query: GetUserQuery): Promise<Result<UserDto | null, AppError>> {
return this.userRepo.findById(query.userId);
}
}
Implements domain ports with real I/O. Depends on Domain ∧ Application.
sql-{aggregate}.repository.ts ∨ in-memory-{aggregate}.repository.ts// infrastructure/repositories/sql-user.repository.ts
export class SqlUserRepository implements UserRepository {
constructor(private readonly db: DatabaseClient) {}
async findById(id: string): Promise<Result<User | null, DomainError>> {
try {
const row = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
if (!row) return ok(null);
return ok(SqlUserMapper.toDomain(row));
} catch (e) {
return err(DomainError.infrastructure('Database query failed'));
}
}
async save(user: User): Promise<Result<void, DomainError>> {
try {
const row = SqlUserMapper.toPersistence(user);
await this.db.query(
'INSERT INTO users (id, email, name, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET name = $4',
[row.id, row.email, row.name, row.createdAt],
);
return ok(undefined);
} catch (e) {
return err(DomainError.infrastructure('Database write failed'));
}
}
}
sql-{aggregate}.mapper.tsEntity.reconstitute() to rebuild domain objects from persistence// infrastructure/mappers/sql-user.mapper.ts
export class SqlUserMapper {
static toDomain(row: UserRow): User {
return User.reconstitute({
id: row.id,
email: row.email,
name: row.name,
createdAt: new Date(row.created_at),
});
}
static toPersistence(user: User): UserRow {
return {
id: user.id,
email: user.email,
name: user.name,
created_at: user.createdAt.toISOString(),
};
}
}
Outermost ring. Entry points, wiring, user-facing I/O.
Depends on Application only (invokes commands/queries, ¬touches domain directly).
{action}-{entity}.dto.ts// presentation/dto/create-user.dto.ts
import { z } from 'zod';
export const CreateUserRequestSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(255),
});
export type CreateUserRequest = z.infer<typeof CreateUserRequestSchema>;
export const CreateUserResponseSchema = z.object({
id: z.string().uuid(),
});
export type CreateUserResponse = z.infer<typeof CreateUserResponseSchema>;
Result errors to HTTP status codes (∨ CLI exit codes)// presentation/di/tokens.ts
export const USER_REPOSITORY = Symbol('UserRepository');
// presentation/di/module.ts
container.bind<UserRepository>(USER_REPOSITORY).to(SqlUserRepository);
class-validator, ¬class-transformer — Zod is the single validation libraryz.infer<typeof Schema> for all derived types| Rule | Rationale |
|---|---|
¬as casting | Breaks type safety; use type guards ∨ Zod .parse() |
¬any | Use unknown + narrowing ∨ generics |
| Symbol-based DI tokens | Type-safe injection, ¬magic strings |
| Generics for collections | Repository<T> base when patterns repeat |
| Type guards for narrowing | function isUser(x: unknown): x is User |
| Discriminated ∪s | { type: 'success'; data: T } | { type: 'error'; error: E } |
// Type guard example
function isValidStatus(value: unknown): value is OrderStatus {
return OrderStatusSchema.safeParse(value).success;
}
// Generic repository port
interface ReadRepository<T, Id = string> {
findById(id: Id): Promise<Result<T | null, DomainError>>;
findAll(): Promise<Result<T[], DomainError>>;
}
| Layer | Test Type | Dependencies |
|---|---|---|
| Domain | Unit | ∅ (pure logic) |
| Application | Unit | ∈-memory adapters for ports |
| Infrastructure | Integration | Real DB / external services |
| Presentation | E2E | Full stack ∨ supertest |
in-memory-{aggregate}.repository.ts// infrastructure/repositories/in-memory-user.repository.ts
export class InMemoryUserRepository implements UserRepository {
private readonly store = new Map<string, User>();
async findById(id: string): Promise<Result<User | null, DomainError>> {
return ok(this.store.get(id) ?? null);
}
async save(user: User): Promise<Result<void, DomainError>> {
this.store.set(user.id, user);
return ok(undefined);
}
async delete(id: string): Promise<Result<void, DomainError>> {
this.store.delete(id);
return ok(undefined);
}
}
user.entity.spec.ts next to user.entity.tscreate-user.handler.spec.ts next to create-user.handler.ts| Anti-Pattern | Why It's Wrong | Fix |
|---|---|---|
| Anemic domain model | Entities are data bags; logic lives ∈ services | Move behavior into entity methods |
| Domain imports infra | Violates dependency rule | Use ports (interfaces ∈ domain) |
App layer imports node:fs | Direct I/O coupling | Define a port, inject adapter |
throw ∈ domain | Untyped, unchecked error flow | Return Result<T, E> |
| Over-abstraction | Port ∀ trivial operation | Abstract only at architectural boundaries |
| Framework coupling ∈ domain | Domain tied to NestJS/Express decorators | Domain must be framework-agnostic |
| Massive inheritance | Fragile base class, tight coupling | Prefer composition over inheritance |
| String-based DI tokens | No type safety, collision risk | Use Symbol('TokenName') |
| Leaking domain types to API | Coupling consumers to internals | Map to DTOs at presentation boundary |
| God services | One service doing everything | One handler per command/query |
| Artifact | Pattern | Example |
|---|---|---|
| Entity | {entity}.entity.ts | user.entity.ts |
| Value Object | {vo}.value-object.ts | email.value-object.ts |
| Domain Event | {entity}-{action}.event.ts | user-created.event.ts |
| Repository Port | {aggregate}.repository.ts | user.repository.ts |
| Repository Adapter | sql-{aggregate}.repository.ts | sql-user.repository.ts |
| ∈-Memory Adapter | in-memory-{aggregate}.repository.ts | in-memory-user.repository.ts |
| Data Mapper | sql-{aggregate}.mapper.ts | sql-user.mapper.ts |
| Command | {action}-{entity}.command.ts | create-user.command.ts |
| Command Handler | {action}-{entity}.handler.ts | create-user.handler.ts |
| Query | {action}-{entity}.query.ts | get-user.query.ts |
| Query Handler | {action}-{entity}.handler.ts | get-user.handler.ts |
| DTO | {action}-{entity}.dto.ts | create-user.dto.ts |
| Domain Service | {name}.domain-service.ts | pricing.domain-service.ts |
| DI Token | UPPER_SNAKE symbol | USER_REPOSITORY |
src/
{module}/
domain/
entities/
value-objects/
events/
ports/
services/
errors/
application/
commands/
queries/
services/
infrastructure/
repositories/
mappers/
adapters/
presentation/
controllers/
dto/
filters/
di/