Apply Clean Architecture principles to define layer boundaries, identify dependency violations, and structure domain vs infrastructure code. Use when designing service boundaries, separating business logic from infrastructure, evaluating hexagonal/onion/ports-and-adapters architecture, structuring module layout, or resolving dependency inversion and circular dependency issues.
68
83%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
—
The risk profile of this skill
Strategic architecture principles for boundaries, dependencies, and layered system design.
Dependency Rule: Dependencies point inward only. Inner layers know nothing about outer layers.
Layers (inner → outer): Entities → Use Cases → Interface Adapters → Frameworks & Drivers
See references/dep-inward-only.md for layer definitions and examples.
Output: Classify as entity, use case, adapter, or framework decision.
Ask:
Output: Dependency violations with corrective actions.
Checklist:
Example:
Violation: Entity imports ORM decorator from infrastructure.
Refactor: Define entity as plain object; map to ORM in repository (adapter layer).Output: Use case interface with input/output ports.
Template:
// Use Case (application layer)
interface CreateOrderUseCase {
execute(input: CreateOrderInput): Promise<CreateOrderOutput>
}
// Input/Output ports (defined by use case)
interface CreateOrderInput {
userId: string
items: OrderItem[]
}
interface CreateOrderOutput {
orderId: string
status: string
}Use cases:
Output: Adapter interfaces (ports) and implementations.
Example:
// Port (defined by use case layer)
interface IOrderRepository {
save(order: Order): Promise<void>
findById(id: string): Promise<Order | null>
}
// Adapter (infrastructure layer)
class PostgresOrderRepository implements IOrderRepository {
async save(order: Order): Promise<void> {
// Map entity to ORM, persist
}
}Output: Framework integrations isolated in outermost layer.
Keep frameworks (Express, NestJS, TypeORM, React) in the infrastructure/adapter layer:
Example:
BAD: Entity uses @Entity decorator from TypeORM.
GOOD: Entity is plain TypeScript; repository maps to TypeORM in infrastructure.Output: ADR with rationale, alternatives, and risks.
Template:
Decision: Extract authentication into separate bounded context.
Rationale: Auth has independent lifecycle and team ownership.
Alternatives:
- Keep in monolith (simpler, tightly coupled)
- Partial boundary (YAGNI, easier to extract later)
Chosen: Full bounded context (clear ownership, independent deployment)
Risks: Network calls add latency; requires distributed transaction handling.BAD: Module A imports B and B imports A.
GOOD: Extract shared contract/module and invert dependencies.
WHY: a cycle means neither module can be understood, tested, or deployed independently — changing one always risks breaking the other, and most build/bundler tools cannot even guarantee a deterministic load order across the cycle.
BAD: Entity imports ORM decorators, framework types, or infrastructure.
GOOD: Entities are plain objects; adapters handle framework mapping.
WHY: once an entity carries an @Entity decorator or a framework base class, every business-rule test must boot that framework too, and swapping the ORM means rewriting the domain model instead of one adapter.
BAD: Controller validates, calculates, and persists data.
GOOD: Controller calls use case; use case orchestrates business logic.
WHY: business logic trapped in a controller can only be exercised through an HTTP request — it MUST be re-implemented for a CLI, a queue consumer, or a test that wants to skip the transport layer entirely.
BAD: Use case instantiates concrete PostgresRepository.
GOOD: Use case depends on IRepository interface; DI provides implementation.
WHY: a use case that names a concrete repository class can never be unit-tested without a real (or heavily mocked) database, and swapping storage engines means editing every use case instead of one adapter.
BAD: Add full hexagonal architecture "in case" of future DB migration.
GOOD: Solve current need; refactor when trigger appears (YAGNI).
WHY: a boundary drawn for a migration that never happens is pure ongoing tax — every future change must thread through an abstraction layer that protects against a scenario nobody triggered.
BAD:
interface IOrderRepository {
findByRawSql(query: string): Promise<OrderRow[]>
}GOOD:
interface IOrderRepository {
findById(id: string): Promise<Order | null>
}WHY: a port that exposes SQL, ORM query builders, or database row shapes ensures the use case layer is coupled to the storage engine even though it depends only on an "abstraction" — the interface must be defined entirely in domain terms, never in persistence terms.
# Find dependency direction violations
rg -n "import.*infrastructure.*from.*domain|import.*adapter.*from.*entity" src# Find circular dependencies
nx graph# Find framework leakage into domain
rg -n "@Entity|@Injectable|@Component" src/domainDependencies: inward-only · acyclic · data crossing boundaries · no framework imports
Components: screaming architecture · stable dependencies
Boundaries: cost awareness · defer decisions · service internal architecture
Entities: purity · rich not anemic · encapsulate invariants · value objects · no persistence awareness
Use Cases: isolation · explicit dependencies · orchestrates not implements · input/output ports · no presentation logic · transaction boundary
Adapters: gateway abstraction · thin controller · mapper translation · presenter formats
Frameworks: DI at edge · domain purity · ORM in infrastructure · web in infrastructure
a1083f4
Also appears in
last in sync Aug 28, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.