← Software design practices guides

How to separate concerns with local specs

How-To Software design practices Intermediate 1101001HOWTO-1101001

HOWTO-1101001Software design practicesIntermediate

This guide shows you how to separate concerns with local SpecDD specs in a spec-driven development workflow.

Separation of concerns means different parts of a system should own different kinds of decisions. A UI component should not quietly become the domain policy. A storage adapter should not decide which data is visible. A service should not absorb every rule just because it coordinates the workflow.

SpecDD helps because local specs let you describe each concern beside the code that owns it. Instead of relying on a large architecture document or tribal memory, each local area has a small contract that says what belongs there and what does not.

Short answer

Identify the distinct concerns in the change, then give each concern a local spec at the smallest useful level. Use Owns and Can modify for authority, Must for behavior that belongs there, Must not for adjacent behavior that must stay out, and References or Depends on when one concern needs another without owning it.

When to use this guide

Use this guide when:

The design idea

Good separation of concerns is not about creating more files for its own sake. It is about placing decisions where they can evolve independently. If display behavior changes, UI specs should guide the change. If a business invariant changes, a model, policy, or domain service spec should guide it. If persistence changes, an adapter spec should own it.

Local specs make this separation visible. They also make overlap reviewable. When two specs both claim the same rule, reviewers can ask which one really owns it. When a task needs edits in several concerns, the team can split the work instead of hiding a cross-boundary change inside one broad task.

Steps

1. Map the concerns

For a feature like “add place to itinerary”, the concerns might be:

These are related, but they are not the same concern.

2. Create one local spec per concern

Use the level that matches the concern:

src/trips/itinerary/itinerary-form.component.sdd
src/trips/itinerary/itinerary-validation.sdd
src/trips/storage/trip-storage.adapter.sdd
src/trips/api/create-itinerary-item.api.sdd

You do not need all of these specs for every change. Start with the concern being changed, then add nearby specs only when the boundary needs to be preserved.

3. Assign ownership

Each spec should own only the files or behavior for its concern:

Spec: Itinerary Validation

Purpose:
  Decide whether an itinerary item can be saved.

Owns:
  ./itinerary-validation.js
  ./itinerary-validation.test.js

Must:
  Reject itinerary items without a place name.
  Reject itinerary items whose day is outside the trip date range.

This spec does not own the UI form or storage adapter.

4. Use references for context, not control

If validation needs the date-range contract, reference it:

References:
  ../dates/date-range.sdd

If a service depends on validation and storage, say so:

Depends on:
  ItineraryValidation
  TripStorage

References and Depends on do not grant permission to edit the referenced concern. They make the relationship visible without merging ownership.

5. Block overlap with non-goals

Use Must not to stop a concern from absorbing adjacent work:

Spec: Itinerary Form

Purpose:
  Capture itinerary item input and show validation feedback.

Must:
  Show a validation message when itinerary validation fails.

Must not:
  Decide whether an itinerary item is valid.
  Save itinerary items directly to storage.

The component can show validation feedback. It should not become the validation authority.

6. Review tasks for boundary creep

A local task should usually fit inside the local boundary:

Tasks:
  [ ] Show the missing-place validation message in the form.

If the task says this instead:

Tasks:
  [ ] Add validation, save behavior, and UI feedback for itinerary items.

split it. That is not one local concern.

Example concern split

UI component spec:

Spec: Itinerary Form

Purpose:
  Capture itinerary item input and show validation feedback.

Owns:
  ./itinerary-form.jsx
  ./itinerary-form.test.jsx

Must:
  Submit place name and day to itinerary behavior.
  Show validation feedback returned by itinerary behavior.

Must not:
  Persist itinerary items directly.
  Decide itinerary validation rules.

Domain behavior spec:

Spec: Itinerary Validation

Purpose:
  Decide whether an itinerary item can be saved.

Owns:
  ./itinerary-validation.js
  ./itinerary-validation.test.js

Must:
  Reject itinerary items without a place name.
  Reject itinerary items outside the trip date range.

Must not:
  Render UI.
  Persist itinerary items.

Storage adapter spec:

Spec: Trip Storage

Purpose:
  Persist and retrieve trips and itinerary items.

Must:
  Save accepted itinerary changes.

Must not:
  Decide whether itinerary input is valid.

The concerns cooperate, but each one owns a different decision.

Common mistakes

How to verify the result

Concerns are separated well when:

← Software design practices guides