SOLID
Overview
Apply SOLID so behavior is easy to change and to test without a pile of mocks.
Companion skills: shape the product with grill → write-spec; prove behavior with tdd + test-design. This skill is only structure and dependency direction.
Core principle: hard to test usually means hard to use — fix the boundaries, don't paper over them with mocks.
Trigger modes (important)
| Mode | When | What to do |
|---|---|---|
| Gate (primary) | After write-spec (or waived design), before write-plan, if the slice adds modules / ports / IO edges | Full workflow below — announce Using solid to … |
| Rescue | Tests need ≥3 mocks; review/PR coupling; mid-tdd stuck on structure | Full workflow; then return to caller |
| Smell only (inside grill) | Comparing approaches that imply different boundaries | Do not load this skill as the main skill. Grill keeps the session; option labels may note one boundary smell ("this binds domain to SQL → mock pile later"). Formal solid waits for Gate |
Default: if product "what/why" is still open → grill, not solid.
When to Use
- Spec approved and the next step would invent packages, ports, or dependency arrows
- Designing or reviewing module boundaries, class/API shape, dependency direction
- Unit tests need many mocks to "isolate"
- Refactors aimed at testability; PR adds coupling / God class
When NOT to use:
- Product decisions still open (use grill)
- Pure glue / drive-by rename / throwaway spike
- Tiny change with no new boundary (existing type, one behavior) — skip Gate; go write-plan or tdd
- As a substitute for grilling product trade-offs
The Iron Law (non-negotiable)
1. New non-trivial behavior lives behind a boundary you can name (SRP).
2. Domain/policy does not depend on IO/framework details (DIP).
3. Fake abstractions invented only to satisfy a test are forbidden (no theater).
Violating the letter is violating the spirit. "Just mock the world" and "God class is fine for now" do not count.
The five, as engineering checks
S — Single Responsibility
- One unit → one reason to change
- If the name needs "and" / "manager of everything" → split
- Separate: orchestration vs policy vs IO vs formatting
O — Open/Closed
- Prefer extending via new types/strategies/composition over editing a core switch forever
- YAGNI: don't pre-build plugin systems; do stop smearing new cases into a blob
L — Liskov Substitution
- Subtypes must honor the caller's contract (pre/postconditions, errors)
- No "inherits for reuse" that surprises callers; prefer composition
I — Interface Segregation
- Small, caller-shaped ports — not one mega-interface
- Callers shouldn't depend on methods they never use
D — Dependency Inversion
- High-level policy depends on abstractions (ports), not on DB/HTTP/FS/SDK concretes
- Adapters at the edge; core stays pure/fakeable
- Signal: unit test needs ≥3 mocks → likely DIP/SRP failure (see test-design)
Workflow (when invoked — Gate or Rescue)
Announce "Using solid to …", then:
- Name the units — what each does, how you use it, what it depends on
- Draw dependency arrows — domain → ports ← adapters (never domain → SQL/HTTP client directly)
- SRP pass — split "and" responsibilities
- ISP pass — trim fat interfaces
- DIP pass — push IO out; inject ports
- Theater check — every new interface must have a product reason, not only a test reason
- Hand off — write-plan (Gate) or resume tdd / review (Rescue). If product questions reopened → grill
Red Flags
- God class / package that "knows the whole feature"
- Domain imports ORM, HTTP client, or UI kit
- Subclass overrides that weaken invariants
- Interface with 15 methods for one caller that uses 2
- New interface used only in tests (no production adapter)
- Unit test mocks half the system to "isolate"
Rationalization Table
| Excuse | Reality |
|---|---|
| "Split later when it hurts" | It already hurts when you need 4 mocks. |
| "Concrete is simpler than a port" | Until the second IO path. Port + one adapter is cheaper than rewrites. |
| "Inheritance reuses code" | Inheritance couples hierarchies; compose instead. |
| "One service interface keeps it tidy" | Fat interfaces force fake methods and ISP debt. |
| "Mocks prove DIP" | Mocks hide DIP failure. Fewer mocks after a real port is the proof. |
| "We're still grilling — run full solid now" | Smell in option labels only. Full solid after what/why is locked. |
Checklist
- Product what/why already locked (or Rescue mid-implementation)
- Each unit has one clear purpose and a name without "and"
- Dependency arrows point inward to policy, not out to IO
- Ports are small; adapters own frameworks
- No test-only abstractions
- Ready for write-plan or tdd/test-design without mock piles
Hand-off
- Product decisions still open → grill → write-spec
- Boundaries locked → write-plan
- Implementing → tdd + test-design
- Shipping → review / commit-and-push