Claude
Skills
Sign in
Back

separation-of-concerns

Included with Lifetime
$97 forever

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.

General

What this skill does


# Separation of Concerns

## Mental Model: Verticals and Horizontals

**Vertical** = all code for ONE feature, grouped together
**Horizontal** = capabilities used by MULTIPLE features

- `features/` — verticals, containing some combination of entrypoint/, commands/, queries/, domain/, infra/
  - commands/ orchestrates write operations (state mutations or external side-effects)
  - queries/ usually queries database directly but can query domain if easier
  - domain/ contains business rules
  - entrypoint/ only needed when exposing external interface (HTTP, CLI, events)
  - infra/ feature-specific infrastructure (mappers, middleware, persistence implementations)
- `platform/` — horizontals, contains `domain/` and `infra/`
  - domain/ depends on nothing — never imports from infra/
  - infra/ CAN depend on domain/ (implements domain contracts)
- `shell/` — app wiring/routing only (no business logic). Registers routes, bootstraps frameworks, connects message brokers. **Not a package entry point** — libraries use `src/index.ts` for that.
- `src/index.ts` — library package entry point. Pure barrel file (only re-export statements, no logic). Only needed for packages consumed by other packages.

**Note on terminology:** CLI subcommands (like `git commit`) are wired in shell/. Write operations in commands/ are CQRS commands — different concepts.

### Application structure

```
features/              platform/              shell/
├── checkout/          ├── domain/            └── cli.ts
│   ├── entrypoint/    │   └── tax-calc/
│   ├── commands/      └── infra/
│   ├── queries/           ├── external-clients/
│   ├── domain/            ├── http/
│   └── infra/             ├── cli/
│       ├── mappers/       ├── persistence/
│       └── persistence/   ├── config/
│                          └── logging/
└── refunds/
    ├── entrypoint/
    ├── commands/
    ├── queries/
    └── domain/
```

### Library package structure

Libraries use the same `features/` + `platform/` structure. The package is NOT the feature — still wrap in `features/{name}/`. Libraries don't need `shell/` unless they wire an app.

```
src/
├── index.ts               ← barrel (pure re-exports, no logic)
├── features/
│   └── extraction/
│       ├── queries/
│       ├── domain/
│       └── infra/
└── platform/
    ├── domain/
    └── infra/
```

Small utility/config/schema packages with no domain logic may be excluded from this architecture entirely.

---

## SoC-001: Always follow the code placement decision tree

🚨 **When unsure where code belongs, follow this decision tree.** Stop at the first match.

### Q1: Does it wire things together at startup?

Registers routes, bootstraps a framework, connects to a message broker, registers CLI subcommands with a framework.

→ **shell/**

**Test:** If you deleted this code, would the app still have all its logic but no way to start?

❌ **Not shell/ if:** It parses input, formats output, contains business logic, or loads/saves data. Those are deeper layers. Also not shell/ if it's a package barrel file re-exporting types for consumers — that's `src/index.ts`.

### Q2: Does it translate between external and internal formats?

Parses HTTP requests, CLI arguments, or queue messages into internal types. Formats internal results into HTTP responses, CLI output (tables, JSON, plain text), or outgoing messages. Maps domain errors to status codes or exit codes. Handles interactive prompts, progress bars, spinners.

→ **entrypoint/**

**Test:** If you changed protocols (HTTP → CLI, CLI → queue consumer, etc.), would you rewrite this code but keep commands/ and domain/ unchanged?

❌ **Not entrypoint/ if:** It loads data, modifies state, persists results, talks to a database, or enforces business rules. If you see load→modify→save, that's commands/, not entrypoint/.

### Q3: Does it orchestrate a write operation?

Loads data, invokes domain logic to modify it, then persists the result. Or coordinates a side-effect through an external service (payment, email, deployment).

→ **commands/**

**Test:** Does it change state? Would "Undo" make sense for this operation?

❌ **Not commands/ if:** It parses external input (HTTP requests, CLI args, queue messages) — that's entrypoint/. It contains the business rules themselves — that's domain/. It only reads data — that's queries/.

### Q4: Does it read and return data without modifying anything?

Loads data, transforms/aggregates it, returns a result. No side effects, no state changes.

→ **queries/**

**Test:** Could you run this 100 times with the same input and get the same result (assuming no external writes)?

❌ **Not queries/ if:** It writes, deletes, sends emails, triggers side effects, or enforces invariants.

### Q5: Is it business logic specific to ONE feature?

Validation rules, state transitions, invariants, domain calculations that only this feature cares about. Repository interfaces (domain contracts for persistence) also live here.

→ **features/{name}/domain/**

**Test:** Does another feature need this? If no → feature domain. If yes → keep reading.

❌ **Not feature domain/ if:** It orchestrates persistence (that's commands/) or is needed by multiple features (that's platform/domain/ or a dedicated domain library package).

### Q6: Is it infrastructure specific to ONE feature?

Repository implementations, response mappers, format adapters, feature-specific middleware. Implements domain contracts or handles protocol/format concerns for this feature only.

→ **features/{name}/infra/**

**Test:** Is this technical plumbing (not business rules) that only this feature needs?

❌ **Not feature/infra/ if:** It contains business rules (that's domain/). It's used by multiple features (that's platform/infra/). It parses external input or invokes commands (that's entrypoint/).

### Q7: Is it shared across features?

**Contains project-specific domain language** (your entity names, your business concepts, your workflow terms)?

→ **platform/domain/** (or a dedicated domain library package)

**Test:** Would a new developer need to understand your business to understand this code?

❌ **Not platform/domain/ if:** It's generic infrastructure with no project-specific concepts.

Shared value objects (Money, Email, Address) that enforce validation → platform/domain/ or a dedicated domain library.

**Shared technical concerns** (HTTP clients, database wrappers, logging, config, response formatters, shared middleware)?

→ **platform/infra/**

Platform/infra/ includes both generic utilities and project-specific conventions for infrastructure concerns (response formatters, error handling middleware).

**Test:** Is it infrastructure that multiple features or entrypoints use?

❌ **Not platform/infra/ if:** It contains business rules or domain invariants. That's platform/domain/.

---

## SoC-002: Dependencies point inward

**What:** Each layer can only depend on layers deeper than itself. Never depend on layers above you.

**Direction:** entrypoint → commands/queries → domain. Infrastructure supports all layers but domain never depends on infrastructure.

| From | Can depend on | Forbidden |
|---|---|---|
| entrypoint/ | commands/, queries/, own feature/infra/, platform/infra/ (restricted — see SoC-012) | domain/, platform/domain/ |
| commands/ | domain/, platform/infra/, platform/domain/, own feature/infra/ | entrypoint/, other features |
| queries/ | domain/ (read-only), platform/infra/, platform/domain/, own feature/infra/ | entrypoint/, commands/ |
| domain/ | platform/domain/ | all infra/ (feature or platform), entrypoint/, commands/, queries/ |
| shell/ | entrypoint/ (to wire routes) | commands/, queries/, domain/ directly |
| src/index.ts (barrel) | any internal module (re-exports only) | must not contain logic |

---

## SoC-003: Features never cross-import

**What:** Code that belongs to one feature stays in that feature's folder. Code used across features lives in platform/ or a dedicated domain library package.

**Why:**
Files: 2
Size: 29.5 KB
Complexity: 40/100
Category: General

Related in General