| name | clean-architecture-skill |
| description | Apply clean architecture principles with vertical slicing, dependency rule, clear layer boundaries, and feature-first organization - language-agnostic |
| license | Apache-2.0 |
| compatibility | opencode |
| metadata | {"audience":"developers","workflow":"architecture","languages":["language-agnostic"]} |
What I do
I help you design and organize software architecture for maintainability and testability:
- Establish Layer Boundaries: Separate code into Presentation, Application, Domain, and Infrastructure layers
- Apply Dependency Rule: Ensure dependencies point inward toward domain logic
- Organize by Feature: Structure code using vertical slicing (feature-first organization)
- Design Contracts: Define clear interfaces between components
- Handle Cross-Cutting Concerns: Manage logging, auth, validation across features
- Create Walking Skeletons: Start with minimal end-to-end slices that prove the architecture
When to use me
Use this skill when:
- Starting a new project and need to structure the codebase
- Refactoring a "big ball of mud" into organized layers
- Planning how to separate business logic from infrastructure
- Designing microservices or bounded contexts
- Setting up testing strategy with proper isolation
- Onboarding developers who need to understand the codebase structure
- Evaluating existing architecture for improvement opportunities
Prerequisites
- Understanding of basic software architecture concepts
- Knowledge of dependency injection
- Familiarity with interfaces and abstractions
- Clear understanding of the business domain
The Goal of Architecture
Enable the development team to:
- Add features with minimal friction
- Change existing features safely
- Remove features cleanly
- Test features in isolation
- Deploy independently when possible
Architectural Principles
1. Vertical Boundaries (Features/Slices)
Organize by feature, not by technical layer.
BAD: Layer-first organization
src/
controllers/
UserController
OrderController
services/
UserService
OrderService
repositories/
UserRepository
OrderRepository
GOOD: Feature-first organization
src/
users/
UserController
UserService
UserRepository
User
orders/
OrderController
OrderService
OrderRepository
Order
Why: Changes to "users" feature stay in users/. High cohesion within features.
2. Horizontal Boundaries (Layers)
Separate concerns into layers with clear dependencies.
┌──────────────────────────────────────┐
│ Presentation │ UI, Controllers, CLI
├──────────────────────────────────────┤
│ Application │ Use Cases, Orchestration
├──────────────────────────────────────┤
│ Domain │ Business Logic, Entities
├──────────────────────────────────────┤
│ Infrastructure │ Database, APIs, External
└──────────────────────────────────────┘
3. The Dependency Rule
Dependencies point INWARD.
Infrastructure → Application → Domain
↓ ↓ ↓
(outer) (middle) (inner)
- Inner layers know NOTHING about outer layers
- Domain has zero dependencies on infrastructure
- Use interfaces to invert dependencies
Language-Agnostic Example:
class UserRepository(ABC):
@abstractmethod
def save(self, user: User) -> None: ...
@abstractmethod
def find_by_id(self, id: UserId) -> Optional[User]: ...
class PostgresUserRepository(UserRepository):
def save(self, user: User) -> None:
def find_by_id(self, id: UserId) -> Optional[User]:
class UserService:
def __init__(self, repo: UserRepository):
self._repo = repo
4. Contracts
Interfaces define boundaries between components.
interface PaymentGateway {
charge(amount: Money, card: CardDetails): Promise<ChargeResult>;
refund(chargeId: string): Promise<RefundResult>;
}
class StripeGateway implements PaymentGateway { }
class PayPalGateway implements PaymentGateway { }
class MockGateway implements PaymentGateway { }
Learning: doc-claimed-purity-vs-reality
A layer rule documented as "the domain layer is pure and has no I/O" but violated by more than 10 call sites is a false contract. Either the rule is wrong for this codebase, or it is unenforced. Leaving the doc and the code in disagreement trains every new developer to ignore BOTH — the next rule they see, they assume is aspirational too. If violations exceed a threshold (10 is a reasonable heuristic), either enforce the boundary with a tool (ESLint no-restricted-imports, eslint-plugin-import boundaries, dependency-cruiser, or madge in CI) OR update the doc to describe what the code actually does. Do not let docs and code drift.
import { PrismaClient } from '@prisma/client'
module.exports = {
rules: {
'no-restricted-imports': ['error', {
patterns: [
{ group: ['@prisma/client', 'pg', 'redis'], message: 'Domain layer must be pure — no I/O imports' }
],
paths: [
{ name: '@prisma/client', message: 'Move to infrastructure/' }
]
}]
},
overrides: [{
files: ['src/domain/**'],
rules: { 'no-restricted-imports': 'error' }
}]
}
Detection:
rg "from '@prisma/client'|from 'pg'|import.*prisma" src/domain/ --type ts -l | wc -l
npx madge --circular --extensions ts src/domain/
Rule: A layer rule with >10 violations is a false contract. Either enforce it with tooling (no-restricted-imports, madge, dependency-cruiser) or rewrite the doc to match reality. Never let architecture docs and code drift — the drift itself becomes the de facto standard.
5. Cross-Cutting Concerns
Concerns that span multiple features: logging, auth, validation, error handling.
Options:
- Middleware/interceptors
- Decorators
- Aspect-oriented approaches
- Base classes (use sparingly)
class LoggingMiddleware:
def handle(self, request, next_handler):
print(f"Request: {request.path}")
response = next_handler(request)
print(f"Response: {response.status}")
return response
Common Architectural Styles
Layered Architecture
Traditional layers: Presentation → Business → Persistence
Pros: Simple, well-understood
Cons: Can become a "big ball of mud" without discipline
Hexagonal Architecture (Ports & Adapters)
Domain at center, adapters around the edges.
┌─────────────────────┐
│ HTTP Adapter │
└─────────┬───────────┘
│
┌─────────────────▼─────────────────┐
│ DOMAIN │
│ ┌─────────────────────────┐ │
│ │ Business Logic │ │
│ │ Use Cases │ │
│ └─────────────────────────┘ │
└─────────────────┬─────────────────┘
│
┌─────────▼───────────┐
│ Database Adapter │
└─────────────────────┘
Ports: Interfaces defined by the domain
Adapters: Implementations that connect to the outside world
Clean Architecture
Similar to Hexagonal, with explicit layers:
- Entities - Enterprise business rules
- Use Cases - Application business rules
- Interface Adapters - Controllers, Presenters, Gateways
- Frameworks & Drivers - Web, DB, External interfaces
Feature-Driven Structure
Frontend Structure
src/
features/
auth/
components/
LoginForm.tsx
SignupForm.tsx
hooks/
useAuth.ts
services/
authService.ts
types/
auth.types.ts
index.ts # Public API
checkout/
components/
hooks/
services/
types/
index.ts
shared/
components/ # Truly shared UI
hooks/ # Truly shared hooks
utils/ # Truly shared utilities
Learning: cross-container-hook-borrowing
A feature container (features/reports/_containers/ReportList/index.tsx) must NEVER import from a sibling container via a relative path (../../_containers/ReportDetail/useReportDetail). Relative cross-container imports create invisible coupling: refactoring ReportDetail silently breaks ReportList, the dependency graph hides the edge, and moving one container forces the other to follow. If a hook is genuinely needed by two containers, extract it to the feature's hooks/ directory (for intra-feature reuse) or to domains/{feature}/hooks/ (for cross-feature reuse). Containers import ONLY from @/-aliased shared locations or from their own feature's hooks/.
import { useReportDetail } from '../../_containers/ReportDetail/useReportDetail'
import { useReportDetail } from '@/features/reports/hooks/useReportDetail'
Detection:
rg "from\s+['\"](\.\./)+.*_containers" --type ts --type tsx
rg "from\s+['\"](\.\./)+.*(use|hook)" --type ts --type tsx -g '*_containers/*'
Rule: Containers import only from @/-aliased shared locations or their own feature's hooks/. If you need a sibling container's hook, extract it to hooks/ or domains/{feature}/hooks/ first. Never reach across container boundaries with relative imports.
Backend Structure
src/
modules/
users/
domain/
User.ts
UserRepository.ts # Interface
application/
CreateUser.ts # Use case
GetUser.ts # Use case
infrastructure/
PostgresUserRepo.ts
presentation/
UserController.ts
UserDTO.ts
orders/
domain/
application/
infrastructure/
presentation/
shared/
domain/ # Shared value objects
infrastructure/ # Shared infra utilities
The Walking Skeleton
Start with a minimal end-to-end slice:
- Thinnest possible feature that touches all layers
- Deployable from day one
- Proves the architecture works
Example Walking Skeleton for E-commerce:
- User can view ONE product (hardcoded)
- User can add it to cart
- User can "checkout" (just logs)
From there, flesh out each feature fully.
Testing Architecture
┌────────────────────────────────────────────┐
│ E2E / Acceptance Tests │ Few, slow, high confidence
├────────────────────────────────────────────┤
│ Integration Tests │ Some, medium speed
├────────────────────────────────────────────┤
│ Unit Tests │ Many, fast, isolated
└────────────────────────────────────────────┘
Test by Layer:
- Domain: Unit tests (most tests here)
- Application: Integration tests with mocked infrastructure
- Infrastructure: Integration tests with real dependencies
- E2E: Critical paths only
Architecture Decision Records (ADRs)
Document significant decisions:
# ADR 001: Use PostgreSQL for Persistence
## Status
Accepted
## Context
We need a database. Options: PostgreSQL, MongoDB, MySQL
## Decision
PostgreSQL for:
- ACID compliance
- Team familiarity
- JSON support for flexibility
## Consequences
- Need PostgreSQL expertise
- Schema migrations required
- Excellent query capabilities
Steps
Step 1: Analyze Current Structure
- Map current code organization
- Identify layer boundaries (or lack thereof)
- Check dependency directions
- Find coupling hotspots
Step 2: Define Target Architecture
- Choose architectural style (Layered, Hexagonal, Clean)
- Define layer boundaries
- Establish naming conventions
- Create dependency rules
Step 3: Create Walking Skeleton
- Implement thinnest possible feature
- Touch all layers end-to-end
- Verify testability
- Validate deployment pipeline
Step 4: Organize by Feature
- Group related code together
- Create feature directories
- Establish public APIs per feature
- Minimize cross-feature dependencies
Step 5: Enforce Dependency Rule
- Create interfaces in inner layers
- Implement in outer layers
- Use dependency injection
- Add architecture tests
Best Practices
Layer Communication
- Inner layers define interfaces
- Outer layers implement interfaces
- Communication through abstractions
- Minimize layer crossing
Feature Organization
- High cohesion within features
- Low coupling between features
- Shared code is truly shared
- Clear public APIs
Testing Strategy
- Domain: Pure unit tests
- Application: Mock infrastructure
- Infrastructure: Real dependencies
- E2E: Critical paths only
Common Issues
Circular Dependencies
Issue: Modules depend on each other
Solution:
- Extract shared code to separate module
- Use interfaces to break cycles
- Apply dependency inversion
Domain Knowing About Infrastructure
Issue: Business logic imports database types
Solution:
- Define interfaces in domain
- Implement in infrastructure
- Use dependency injection
"Utils" Package Growing Forever
Issue: Shared utilities becoming a dumping ground
Solution:
- Question if code is truly shared
- Move to feature if only used there
- Create focused utility modules
Red Flags in Architecture
- Circular dependencies between modules
- Domain depending on infrastructure
- Framework code in business logic
- No clear boundaries between features
- Shared mutable state across modules
- "Util" or "Common" packages that grow forever
- Database schema driving domain model
Verification Commands
After restructuring:
pip install pydeps && pydeps src --no-output -T png
npx madge --circular src/
grep -r "from.*infrastructure" src/domain/
grep -r "import.*database" src/domain/
Architecture Verification Checklist: