agentleFS
ovargasovargas/virtual-team/skills/design-principles/SKILL.md

design-principles

Skill14 starsChanged 8 months agoMITView on GitHub

Use when writing production code — enforces dependency inversion and testable design through checkpoint questions before writing function signatures and constructors

24 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

13 rules. 0 are common, 10 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 21 edited versions we read keep these.

  • 10

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Read stack.md and check the design: field.

    line 10
  2. Distinctive:

    Before writing a function signature, constructor, or method — pause and ask:

    line 30
  3. Distinctive:

    If the answer is "nothing" — no checkpoint needed.

    line 36
  4. Distinctive:

    If the answer is "something" — continue to question 2.

    line 38
  5. Very specific:

    If you're about to write new, import a concrete client, or call a constructor for something that does I/O — you're creating, not accepting.

    line 59
  6. Distinctive:

    Not every language has explicit interfaces.

    line 82
  7. Distinctive:

    TypeScript/Java/C#: Use interfaces or abstract types at boundaries

    line 83
  8. Distinctive:

    Python: Use protocols (typing.Protocol) or ABC at boundaries

    line 84
  9. Distinctive:

    Not every function needs this checkpoint.

    line 92
  10. Distinctive:

    Every application has one place where concrete implementations are wired together.

    line 105
  11. Very specific:

    If you find yourself writing new ConcreteService() outside the composition root, pause.

    line 118
  12. Distinctive:

    When you follow both:

    line 127
  13. Distinctive:

    Before marking implementation steps complete:

    line 137

“Common” is measured: kept by at least 6 in 10 of the 21 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Design Principles

Mode Selection

Read stack.md and check the design: field. If the field is missing, default to recommended.

design: valueBehavior
strictEvery function boundary must pass the checkpoint. Rewrite if it fails.
recommendedCheck at key boundaries (constructors, service functions, handlers). Flag skips.
offNo enforcement. Skip the rest of this skill.

If design: off — stop reading.

Core Principle

Accept what you need. Don't create what you use.

A function that creates its own collaborators is a function that can't be tested in isolation. A function that receives them can be tested with any substitute — mock, stub, fake, or the real thing.

This is Dependency Inversion: depend on the behavior you need, not the thing that provides it.

The Checkpoint

Before writing a function signature, constructor, or method — pause and ask:

1. "What does this function need from the outside world?"

List the external collaborators: database access, HTTP clients, file system, other services, time, randomness.

If the answer is "nothing" — no checkpoint needed. Pure logic is already testable.

If the answer is "something" — continue to question 2.

2. "Am I accepting it or creating it?"

<Good> ``` // Accepts its dependency — caller decides what to pass function createUser(repo: UserRepository, data: NewUser): Promise<User> ``` </Good><Bad> ``` // Creates its own dependency — hardwired to one implementation function createUser(data: NewUser): Promise<User> { const repo = new PostgresUserRepository(getConnectionPool()) // ... } ``` </Bad>

If you're about to write new, import a concrete client, or call a constructor for something that does I/O — you're creating, not accepting. Restructure.

3. "Am I depending on a behavior or an implementation?"

The parameter type should describe what you need, not who provides it.

<Good> ``` // Depends on behavior — any implementation works interface UserRepository { findById(id: string): Promise<User | null> save(user: User): Promise<void> } ``` </Good><Bad> ``` // Depends on implementation — locked to Postgres function createUser(repo: PostgresUserRepository, data: NewUser) ``` </Bad>

Not every language has explicit interfaces. The principle still applies:

  • TypeScript/Java/C#: Use interfaces or abstract types at boundaries
  • Python: Use protocols (typing.Protocol) or ABC at boundaries
  • Go: Interfaces are implicit — accept the interface, return the struct
  • Ruby/JS: Duck typing — document the expected shape, accept any object that quacks

The key: your function's contract is with a shape, not a name.

When This Matters Most

Not every function needs this checkpoint. Focus on boundaries — where your code meets the outside world:

BoundaryCheckpoint appliesWhy
Service/use-case functionsYesThese orchestrate — they need collaborators
Constructors / factory functionsYesThis is where dependencies get wired
Route handlers / controllersYesThese connect HTTP to business logic
Pure business logicNoNo external dependencies to invert
Simple utilitiesNoformatDate() doesn't need dependency injection
Glue code / composition rootNoThis is where you wire concretes — that's its job

The Composition Root

Every application has one place where concrete implementations are wired together. This is the composition root — the entry point, the main function, the DI container setup.

Concrete instantiation belongs here, not in business logic:

// composition root — the ONE place that knows about concretes
const repo = new PostgresUserRepository(pool)
const emailer = new SmtpEmailService(config)
const handler = new CreateUserHandler(repo, emailer)

app.post('/api/users', (req, res) => handler.handle(req, res))

If you find yourself writing new ConcreteService() outside the composition root, pause. You're wiring in the wrong place.

Relationship with TDD

This skill and TDD are complementary:

  • TDD asks: "What test proves this works?"
  • This skill asks: "Can this be tested in isolation?"

When you follow both:

  1. TDD says write the test first
  2. You try to write the test and realize you can't mock the dependency
  3. This skill says: that's because the function creates its dependency — restructure it to accept it
  4. Now the test is easy: pass a mock/stub, verify behavior

On mocking: TDD's guidance to prefer "real code over mocks" is about not mocking to paper over bad design. When the design is right — when dependencies are injected as abstractions — mocking is the clean, correct approach for unit tests. Integration tests still use real implementations.

Verification Checklist

Before marking implementation steps complete:

All modes:

  • Service functions accept their collaborators as parameters
  • No concrete I/O clients instantiated inside business logic
  • Parameter types describe behavior, not implementation (where the language supports it)

Strict additionally:

  • Every boundary function passes the three checkpoint questions
  • Composition root is clearly identifiable — concretes wired in one place
  • Unit tests use mocks/stubs for external collaborators; integration tests use real implementations

Common Resistance

ThoughtResponse
"Passing dependencies everywhere is boilerplate"It's explicit. Explicit dependencies are debuggable. Hidden ones aren't.
"This is over-engineering for a small project"If you're writing tests, you need testable code. Size doesn't change that.
"I'll refactor to abstractions later"Later never comes. The concrete dependency spreads through the codebase.
"My language doesn't have interfaces"You don't need the keyword. Accept the shape, not the name.
"DI containers are complex"You don't need a container. Constructor parameters are dependency injection.

Integration

This skill is loaded by:

  • /virtual-team:implement — Layer 0 (behavioral discipline), alongside TDD
  • /virtual-team:flow — inherited through /virtual-team:implement
  • SDD implementer subagents — loaded when executing plan tasks

The skill self-configures by reading stack.md. Consumers load it the same way regardless of mode.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.