separation of concerns
npx skills add https://github.com/ntcoding/claude-skillz --skill Separation of Concerns
Skill 文档
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 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/
â âââ commands/ âââ infra/
â âââ queries/ âââ ext-clients/
â âââ domain/
âââ refunds/
âââ entrypoint/
âââ commands/
âââ queries/
âââ domain/
Entrypoint Responsibilities
What: Thin mapping layer between external world and commands/queries.
Pattern:
- Parse external input into command or query object
- Invoke command or query
- Map result to external response
class OrderController {
constructor(
private placeOrder: PlaceOrderCommand,
private getOrderSummary: GetOrderSummaryQuery
) {}
post(req: HttpRequest): HttpResponse {
const cmd = parseOrderCommand(req.body)
const result = this.placeOrder.execute(cmd)
return mapToHttpResponse(result)
}
get(req: HttpRequest): HttpResponse {
const orderId = req.params.id
const summary = this.getOrderSummary.execute(orderId)
return mapToHttpResponse(summary)
}
}
Dependency Rules:
- â CAN depend on: commands/, queries/, platform/infra/
- â FORBIDDEN: domain/ (entrypoint never imports domain directly)
Behavioral Rules:
- â NO orchestration (that’s commands/)
- â NO domain logic (that’s domain/)
- â NO data fetching (that’s queries/)
- â Owns input parsing and output mapping
Commands
What: Orchestrate write operations that mutate state. Commands MUST go through the domain layer.
Why strict layering: Commands change state. Domain invariants must be enforced. Skipping domain/ means business rules can be violated.
Pattern:
- Receive command input (already parsed by entrypoint)
- Load domain aggregates/entities
- Execute domain logic (validation, state transitions)
- Persist changes
- Return result
class ApproveRefundCommand {
constructor(private refundRepository: RefundRepository) {}
execute(input: ApproveRefundInput): Refund {
const refund = this.refundRepository.get(input.refundId)
refund.approve(input.approvedBy, input.reason)
this.refundRepository.save(refund)
return refund
}
}
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
class GetOrderSummaryQuery {
constructor(private db: DatabaseClient) {}
execute(orderId: string): OrderSummary {
const row = this.db.query('SELECT ... FROM orders WHERE id = ?', [orderId])
if (!row) throw new OrderNotFoundError(orderId)
return new OrderSummary(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 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 command or query
â â âââ commands/ â write operations, strict layering
â â âââ queries/ â read operations, minimal layering
â â âââ domain/ â business rules (required for commands)
â â
â âââ order-dashboard/ â read-only feature with external API
â â âââ entrypoint/
â â âââ queries/
â â
â âââ reporting/ â internal query library
â âââ queries/ â no entrypoint needed, consumed by other features
â
âââ platform/
â âââ domain/ â shared business rules
â âââ infra/ â technical concerns
â
âââ shell/
âââ cli.ts
Mandatory Checklist
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
Do not proceed until all checks pass.