name: Separation of Concerns description: "Enforces code organization using features/ (verticals), platform/ (horizontals), and shell/ (thin wiring). Triggers on: code organization, file structure, where does this belong, new file creation, refactoring." version: 2.4.0
Separation of Concerns
Principles
- Separate external clients from domain-specific code
- Separate feature-specific from shared capabilities
- Separate intent from execution
- Separate functions that depend on different state
- Separate functions that don't have related names
Mental Model: Verticals and Horizontals
Vertical = all code for ONE feature, grouped together Horizontal = capabilities used by MULTIPLE features
All three top-level folders are mandatory:
-
features/— verticals, each with entrypoint/, use-cases/, domain/- use-cases/ orchestrates workflow; domain/ contains business rules and behavior
-
platform/— horizontals, only containsdomain/andinfra/(nothing else) -
shell/— thin wiring/routing only (no business logic)
infra/ lives in platform/infra/, not inside features.
features/ platform/ shell/
├── checkout/ ├── domain/ └── cli.ts
│ ├── entrypoint/ │ └── tax-calc/
│ ├── use-cases/ └── infra/
│ └── domain/ └── ext-clients/
└── refunds/
├── entrypoint/
├── use-cases/
└── domain/
Entrypoint Responsibilities
What: Thin mapping layer between external world and use cases.
Pattern:
- Parse external input into command/request object
- Invoke use case
- Map result to external response
// ✅ GOOD - thin mapping, no orchestration, no domain
class OrderController {
constructor(private placeOrder: PlaceOrderUseCase) {}
handle(req: HttpRequest): HttpResponse {
const command = parseOrderCommand(req.body)
const result = this.placeOrder.execute(command)
return mapToHttpResponse(result)
}
}
Dependency Rules:
- ✅ CAN depend on: use-cases/, platform/infra/
- ❌ FORBIDDEN: domain/ (direct domain imports are not allowed)
Behavioral Rules:
- ❌ NO orchestration (that's use-cases/)
- ❌ NO domain logic (that's domain/)
- ✅ Owns input parsing and output mapping
Principle 1: Separate external clients from domain-specific code
What: Generic wrappers for external services (APIs, databases, SDKs) live separately from code that uses them in domain-specific ways.
Why: Domain logic mixed with external service details is harder to understand and evolve. Separating them keeps domain logic pure and focused.
How:
- Ask: "Would the creators of this external service recognize this code?"
- YES → external-clients/
- NO → your domain code
❌ BAD:
platform/infra/external-clients/order-total.ts ← domain logic in infra
features/checkout/stripe-api.ts ← external client in feature
✅ GOOD:
platform/infra/external-clients/stripe.ts ← generic: charge, refund, subscribe
features/checkout/payment-processing.ts ← OUR domain logic using stripe
Principle 2: Separate feature-specific from shared capabilities
What: Code that belongs to one feature stays in that feature's folder. Code used across features lives in a shared location named for what it IS.
Why: When shared logic is buried in one feature, other features either import across boundaries (coupling) or duplicate the logic (divergence). Both cause bugs.
How:
- Ask: "Does this conceptually belong to one feature?"
- YES → keep in features/
- NO → extract to platform/, name it for what it IS
❌ BAD - buried in one feature:
features/checkout/tax-calculator.ts
features/refunds/refund.ts ← imports ../checkout/tax-calculator
❌ BAD - duplicated:
features/checkout/tax-calculator.ts
features/refunds/tax-calculator.ts ← rules diverge over time
✅ GOOD - extracted to platform:
features/checkout/
features/refunds/
platform/domain/tax-calculation/ ← shared domain logic
Principle 3: Separate intent from execution
What: High-level flow visible at one abstraction level. Implementation details in lower levels.
Why: When intent and execution are mixed, you can't see what the code does without reading every line. Changes to one step's implementation ripple through unrelated code.
How:
- Ask: "Can I see the high-level flow without reading every line?"
- NO → extract details into named functions/methods
// ❌ BAD - can't see flow, details obscure intent
async function checkout(cart: Cart) {
const ctx = new CheckoutContext()
try {
const validation = await validateCart(cart)
if (!validation.success) { /* 10 lines of error handling */ }
const payment = await processPayment(cart)
if (!payment.success) { /* 10 lines of rollback */ }
// ... 30 more lines
} catch (e) { await cleanup(ctx); throw e }
}
// ✅ GOOD - flow visible, drill into details as needed
function checkout(cart: Cart, payment: PaymentDetails) {
const validatedCart = cart.validate()
const receipt = paymentService.process(validatedCart.total, payment)
const order = Order.create(validatedCart, receipt)
confirmationService.send(order)
return order
}
Principle 4: Separate functions that depend on different state
What: Functions that depend on different state (different fields, databases, services, config) belong in different modules.
Why: Different state dependencies mean different reasons to change, different testing strategies, and different failure modes.
How:
- List the fields/dependencies in a class
- For each method, note which it uses
- Methods cluster around different state? → split into separate classes
❌ BAD:
class OrderService {
db, emailClient, templateEngine
save() → uses db
find() → uses db
sendConfirmation() → uses emailClient, templateEngine
}
✅ GOOD:
class OrderRepository { db }
class OrderNotifications { emailClient, templateEngine }
Principle 5: Separate functions that don't have related names
What: Functions in the same module should have names that relate to a common concept.
Why: Unrelated names signal unrelated responsibilities. If you can't name the module after what the functions have in common, they probably don't belong together.
How:
- Look at the function names in a module
- Can you describe what they have in common in one phrase?
- NO → split them into separate modules
❌ BAD - order-helpers.ts:
calculateOrderTotal()
formatOrderForInvoice()
validateOrderForShipping()
assessOrderFraudRisk()
→ all operate on "order" but change for different reasons:
pricing rules, invoice formatting, shipping constraints, fraud detection
✅ GOOD - split by why they change:
order-pricing.ts: calculateTotal(), applyDiscounts()
invoice-formatting.ts: formatForInvoice(), formatLineItems()
shipping-validation.ts: validateForShipping(), checkWeightLimits()
fraud-detection.ts: assessFraudRisk(), flagSuspiciousPatterns()
Package Structure
/food-delivery/
├── features/
│ ├── order-placement/
│ │ ├── entrypoint/ ← thin, invokes use-case
│ │ ├── use-cases/ ← orchestration
│ │ └── domain/ ← business rules
│ │
│ └── driver-tracking/
│ ├── entrypoint/
│ ├── use-cases/
│ └── domain/
│
├── platform/
│ ├── domain/ ← shared business rules
│ └── infra/ ← technical concerns (infra lives here, not in features)
│
└── shell/
└── cli.ts
Mandatory Checklist
When designing, implementing, refactoring, or reviewing code, complete this checklist:
- Verify features/, platform/, shell/ exist at the root of the package
- Verify platform/ contains only domain/ and infra/
- Verify each feature contains only entrypoint/, use-cases/, domain/
- Verify shell/ contains no business logic
- Verify code belonging to one feature is in features/[feature]/
- Verify shared business logic is in platform/domain/ and no dependencies between features
- Verify external service wrappers are in platform/infra/
- Verify custom folders (steps/, handlers/) are inside domain/, not use-cases/
- Verify each function relies on same state as others in its class/file and name aligns
- Verify each file name relates to other files in its directory
- Verify each directory name describes what all files inside have in common
- Verify use-cases/ contains only use-case files (no nested folders, no helper files)
- Verify no generic type-grouping files (types.ts, errors.ts, validators.ts) spanning multiple capabilities
- Verify entrypoint/ is thin (parse input → invoke use-case → map output) and never imports from domain/
Do not proceed until all checks pass.
chat Comments (0)
Sign in to join the discussion and leave a comment.
Skill Details
Related Skills
Build your own?
Join 12,000+ developers contributing to the Claude ecosystem.
No comments yet. Be the first to share your thoughts!