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.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
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.5.1
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, containing some combination of entrypoint/, commands/, queries/, domain/
commands/ orchestrates write operations; MUST go through domain/
queries/ handles read operations; MAY bypass domain/
domain/ contains business rules (required if commands/ exists)
entrypoint/ only needed when exposing external interface (HTTP, CLI, events)
platform/ — horizontals, only contains domain/ and infra/ (nothing else)
shell/ — thin wiring/routing only (no business logic)
infra/ lives in platform/infra/, not inside features.
Note: Commands should have a single transaction boundary. If you need external service calls (payment, email), use the outbox pattern—persist domain events in the same transaction, process them asynchronously.
Dependency Rules:
✅ MUST depend on: domain/ (this is the point)
✅ CAN depend on: platform/infra/, platform/domain/
❌ FORBIDDEN: other features' commands/, queries/, or domain/
Behavioral Rules:
✅ One command = one transaction boundary
✅ All business logic delegated to domain/
❌ NO direct database queries (use repositories from domain/)
❌ NO business rules in command itself
Naming: Verb phrase matching the action. place-order.ts, cancel-subscription.ts, approve-refund.ts. Menu test: would this appear on a UI menu?
Queries
What: Handle read operations. Queries MAY bypass domain/ for simplicity and performance.
Why minimal layering: Queries don't mutate state. No invariants to protect. Optimize for read performance and simplicity.
Pattern:
Receive query input (already parsed by entrypoint)
Fetch data (directly from repository/database)
Map to response DTO
Return result
classGetOrderSummaryQuery {
constructor(privatedb: DatabaseClient) {}
execute(orderId: string): OrderSummary {
const row = this.db.query('SELECT ... FROM orders WHERE id = ?', [orderId])
if (!row) thrownewOrderNotFoundError(orderId)
returnnewOrderSummary(row.id, row.status, Money.from(row.total))
}
}
Dependency Rules:
✅ CAN depend on: platform/infra/, platform/domain/
✅ CAN import: domain/ value objects (for validation/typing)
❌ FORBIDDEN: domain/ services or aggregates
❌ FORBIDDEN: commands/
Behavioral Rules:
✅ Read-only, no side effects
✅ Can query database directly (no repository required)
✅ Can import value objects from domain/ for response typing
❌ NO state mutations
❌ NO business rule enforcement (queries trust the data)
Naming: Verb phrase describing what you're fetching. get-order-summary.ts, list-pending-refunds.ts, search-products.ts.
Query-only features: Features that only read data need only queries/. No entrypoint/ required if queries are consumed internally by other features. No domain/ required since no invariants to protect.
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 intentasyncfunctioncheckout(cart: Cart) {
const ctx = newCheckoutContext()
try {
const validation = awaitvalidateCart(cart)
if (!validation.success) { /* 10 lines of error handling */ }
const payment = awaitprocessPayment(cart)
if (!payment.success) { /* 10 lines of rollback */ }
// ... 30 more lines
} catch (e) { awaitcleanup(ctx); throw e }
}
// ✅ GOOD - flow visible, drill into details as neededfunctioncheckout(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()
When designing, implementing, refactoring, or reviewing code, complete this checklist:
Structure:
Verify features/, platform/, shell/ exist at the root
Verify platform/ contains only domain/ and infra/
Verify each feature contains only entrypoint/, commands/, queries/, domain/ (all optional; entrypoint/ only for external interfaces)
Verify shell/ contains no business logic
Commands (write path):
5. [ ] Verify commands/ exists if feature mutates state
6. [ ] Verify domain/ exists if commands/ exists
7. [ ] Verify every command imports from domain/ (commands MUST use domain)
8. [ ] Verify commands contain no business rules (delegated to domain/)
9. [ ] Verify commands/ contains only command files (no nested folders, no helpers)
Queries (read path):
10. [ ] Verify queries/ imports only value objects from domain/ (not services/aggregates)
11. [ ] Verify queries never mutate state
12. [ ] Verify queries/ contains only query files (no nested folders, no helpers)
Entrypoint:
13. [ ] Verify entrypoint/ is thin (parse → invoke command/query → map output)
14. [ ] Verify entrypoint/ never imports from domain/
15. [ ] Verify entrypoint/ only imports from commands/, queries/, platform/infra/
General:
16. [ ] Verify no dependencies between features
17. [ ] Verify shared business logic is in platform/domain/
18. [ ] Verify external service wrappers are in platform/infra/
19. [ ] Verify no generic type-grouping files (types.ts, errors.ts) spanning capabilities