Purpose: Prevent large-file splits from moving mixed responsibilities into smaller files without correcting ownership and dependency boundaries.
Use this guide when: A task splits a large file or package, extracts a subsystem, moves a cluster of functions, or claims to resolve structural or architectural debt.
A smaller file is not evidence of architectural improvement. A refactor improves architecture only when every reason to change has one explicit owner and dependencies point from outer layers toward inner layers.
Do not group functions merely because they are adjacent or serve the same CLI command. Assign them according to the responsibility they own.
| Owner | Owns | Must not own |
|---|---|---|
| Controller | Input parsing, use-case invocation, final delivery side effect | Business rules, SQL, persistence mapping |
| Use case | Application workflow and coordination of domain behavior | Concrete infrastructure, raw external records |
| Domain service | Pure business rules and transformations | Filesystem, database, network, CLI behavior |
| Port | Typed capability required by an inner layer | Concrete implementation details |
| Adapter | I/O, external schema knowledge, raw-record mapping | Application policy or cross-use-case orchestration |
| Presenter | Conversion of typed results into a user-facing representation | Persistence queries or business decisions |
| Composition root | Construction and wiring of concrete implementations | Business rules or data transformation |
Complete this table before editing implementation:
| Responsibility | Current location | Reason to change | Target owner | Typed boundary | Public test surface |
|---|---|---|---|---|---|
<behavior> |
<module> |
<independent change driver> |
<owner> |
<request/result/port> |
<test module and public symbol> |
Then write the intended dependency graph:
controller
-> typed application request
use case
-> port
adapter
composition root
-> use case + concrete adapters
List forbidden edges explicitly. Typical examples are:
controller -X-> database adapter
application -X-> concrete adapter
domain -X-> I/O
adapter -X-> controller
presenter -X-> repository
Do not begin the split when any of these is true:
Resolve the design first or narrow the task to a safe preparatory change.
Ports precede adapters. Creating a concrete adapter first and calling it from existing outer layers establishes the adapter as a de facto public dependency and recreates the coupling the refactor is intended to remove.
Follow the detailed matrix in testing-guide.md. Each test module imports the public surface of the layer it tests.
The boundary is wrong when:
type: ignore to satisfy the consumer.Repeat the responsibility inventory for every resulting module:
| Resulting module | Single owner | Reason to change | Allowed imports | Public test |
|---|---|---|---|---|
<module> |
<owner> |
<one change driver> |
<inner layers or ports> |
<test> |
Search the resulting tree for the old responsibility markers, including I/O imports, schema or table names, raw record aliases, duplicated transformations, and private-helper imports. The search must show that each responsibility has one canonical home.
A structural split relocates code into smaller files while preserving responsibility ownership and dependencies. It can be useful, but it must be described as structural work.
An architectural split changes ownership, introduces explicit boundaries, and makes invalid dependencies harder or impossible. Do not claim that architectural debt is resolved when only a structural split was performed.