← Software design practices guides

How to maintain the Single Responsibility Principle with SpecDD

How-To Software design practices Intermediate 1101000HOWTO-1101000

HOWTO-1101000Software design practicesIntermediate

This guide shows you how to use spec-driven development to maintain the Single Responsibility Principle.

The Single Responsibility Principle is often summarized as “one reason to change.” In practice, that means a module, service, model, adapter, component, or job should have one coherent responsibility, not a collection of unrelated decisions that happened to accumulate in the same file.

SpecDD is an amazing way to make this principle operational because the responsibility is not just a guideline in a style guide. It becomes a local, source-adjacent contract that humans and agents consult while changing code.

Short answer

Use Purpose to name the unit’s responsibility, Owns and Can modify to define the local boundary, Must to state the behavior that belongs there, and Must not to block responsibilities that should stay elsewhere. Keep Tasks small enough to complete inside the local boundary. During review, reject changes that add a second unrelated reason for the unit to change.

When to use this guide

Use this guide when:

The design idea

SRP is not a rule that every file must be tiny. A unit can be substantial if all of its behavior supports one coherent responsibility. The problem starts when a unit needs to change for unrelated reasons: storage changes, validation changes, UI layout changes, policy changes, API compatibility changes, and reporting changes all pushing on the same place.

Specs help because they force a naming decision. If the Purpose line cannot explain the unit without using “and” for unrelated concerns, the unit may already have more than one responsibility. If the Must rules describe different kinds of work, the spec is showing you where to split.

Steps

1. Choose the unit

Pick the smallest useful unit whose responsibility you want to protect:

Do not start with the whole product area unless the responsibility is truly module-wide.

2. Write one clear purpose

Good:

Spec: Trip Storage

Purpose:
  Persist and retrieve trips and itinerary items.

Weak:

Spec: Trip Storage

Purpose:
  Save trips, validate itinerary rules, manage display order, and decide which trips are visible.

The weak purpose mixes persistence, validation, ordering, and visibility policy. Those may all matter to trip behavior, but they are different reasons to change.

3. Define owned files and behavior

Use Owns to make the local boundary concrete:

Owns:
  ./trip-storage.js
  ./trip-storage.test.js

Then use Must for behavior that belongs inside the responsibility:

Must:
  Trip changes are saved.
  Previously saved trips are loaded when the app starts.
  Save failures are reported in a way the itinerary can show to the person.

These rules are cohesive. They all describe storage behavior.

4. Add non-goals

Use Must not for responsibilities that are plausible mistakes:

Must not:
  Change place names.
  Move itinerary items between days.
  Decide which itinerary items are visible.

This is where SpecDD turns SRP from advice into a guardrail. The spec does not merely say what storage does. It also says which tempting adjacent decisions storage must not absorb.

5. Keep tasks inside the responsibility

Good:

Tasks:
  [ ] Report save failures to itinerary behavior.
  [ ] Add a check for loading previously saved trips.

Wrong for this spec:

Tasks:
  [ ] Add itinerary validation for missing place names.

That task may be important, but it belongs in the itinerary validation spec, not the storage spec. A task that requires touching unrelated files is a sign that the task or the spec boundary needs review.

6. Review changes against the responsibility

During code review, ask:

This review is especially useful for agent-generated code because the nearest file is often not the right design home.

Full example

Spec: Trip Storage

Purpose:
  Persist and retrieve trips and itinerary items.

Owns:
  ./trip-storage.js
  ./trip-storage.test.js

Must:
  Trip changes are saved.
  Previously saved trips are loaded when the app starts.
  Save failures are reported in a way the itinerary can show to the person.

Must not:
  Change place names.
  Move itinerary items between days.
  Decide which itinerary items are visible.

Depends on:
  browser local storage

Tasks:
  [ ] Add failure reporting for save errors.

Done when:
  Save failure behavior is covered by a check.
  Itinerary validation behavior remains outside trip storage.

This spec gives the storage unit one reason to change: persistence behavior. It can change when storage changes. It should not change because the team wants a new itinerary validation rule or a new display policy.

Common mistakes

How to verify the result

The unit is respecting SRP when:

← Software design practices guides