← Software design practices guides

How to prevent cross-layer coupling

How-To Software design practices Intermediate 1101003HOWTO-1101003

HOWTO-1101003Software design practicesIntermediate

This guide shows you how to prevent cross-layer coupling with SpecDD in a spec-driven development workflow.

Cross-layer coupling happens when code in one layer reaches into another layer in a way the architecture did not intend. A UI component imports a storage adapter. An API handler reaches around a domain service. A model calls infrastructure. A background job duplicates business rules because calling the domain layer looks inconvenient.

These shortcuts are usually local and reasonable in the moment. Over time they make the system harder to test, change, and explain. SpecDD gives you a concrete place to define the layer rule and check every change against it.

Short answer

Define layer direction in the spec that owns the architecture boundary. Use Depends on for allowed collaborators, Forbids for blocked imports, paths, dependencies, tools, or access patterns, and Must not for behavior a layer must not own. Local specs should narrow the rule, not reverse it. During review, inspect imports, calls, writes, and tasks for layer violations.

When to use this guide

Use this guide when:

The design idea

Layers are useful because they keep different kinds of change separate. UI layout can change without rewriting domain rules. Persistence can change without changing the business invariant. The API transport can change without moving core behavior.

The important design question is not “how many layers should this app have?” It is “which directions are allowed, and which directions are forbidden?” SpecDD can encode that answer in the parent spec and let child specs inherit it.

Steps

1. Name the layers

Start with the architecture you actually use:

Spec: Trips Module

Purpose:
  Provide trip planning behavior through UI, application services, domain rules, and storage adapters.

Structure:
  ./ui/: UI components and hooks
  ./services/: application orchestration
  ./domain/: domain models, policies, and invariants
  ./adapters/: external storage and API boundaries

Structure helps humans understand the layout. It does not replace ownership or dependency rules.

2. Define allowed direction

Use Must or Depends on to state intended direction:

Must:
  UI components call services for trip changes.
  Services coordinate domain rules and adapters.
  Domain rules remain independent of UI, API, and storage frameworks.

Depends on:
  services depend on domain
  services depend on adapters
  UI depends on services

Keep dependency statements meaningful. Do not list every import that happens to exist.

3. Forbid reverse access

Use Forbids for hard boundaries:

Forbids:
  UI importing ./adapters/*
  domain importing ./ui/*
  domain importing ./adapters/*
  adapters importing ./ui/*

Use Must not for behavior boundaries:

Must not:
  UI components decide whether itinerary input is valid.
  Storage adapters decide which itinerary items are visible.

Together, these rules block both code-level coupling and responsibility-level coupling.

4. Use adapters at external boundaries

External systems should usually enter through adapter specs:

Spec: Trip Storage Adapter

Purpose:
  Persist and retrieve trips through browser local storage.

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

Must:
  Save accepted trip changes.
  Load previously saved trips when the app starts.

Must not:
  Decide itinerary validation rules.
  Render UI.

Depends on:
  browser local storage

This keeps storage details out of UI and domain code.

5. Keep local tasks inside the layer

Good task in a UI spec:

Tasks:
  [ ] Show the itinerary validation message returned by the service.

Layer-violating task:

Tasks:
  [ ] Save itinerary items directly from the component.

The second task should be blocked if UI is forbidden from importing storage adapters.

6. Review imports and calls

During review, check:

Layer coupling often enters through a small convenience change, not a deliberate architecture rewrite.

Example layer boundary

Spec: Trips Module Architecture

Purpose:
  Keep trip planning behavior separated across UI, services, domain rules, and adapters.

Structure:
  ./ui/: UI components and hooks
  ./services/: application orchestration
  ./domain/: domain rules and invariants
  ./adapters/: external storage and API boundaries

Must:
  UI components call services for trip changes.
  Services coordinate domain rules and adapters.
  Domain rules remain independent of UI, API, and storage frameworks.

Must not:
  UI components decide itinerary validation rules.
  Adapters decide itinerary visibility or validation.

Forbids:
  UI importing ./adapters/*
  domain importing ./ui/*
  domain importing ./adapters/*

This spec gives future changes a clear dependency direction. A child spec may add a narrower local rule, but it cannot silently reverse the parent architecture.

Common mistakes

How to verify the result

Cross-layer coupling is controlled when:

← Software design practices guides