domain
A logical area of the problem, and an explicit statement of what belongs to it.
export domain Rendering:
description: Turning a document into pixels.
covers:
- The scene graph and its traversal
- Rasterisation and caching
- Colour managementA domain is a region of the problem. Rendering. Billing. Export. Identity.
It is not a folder, though it often has one. It is not a module, though it usually contains several. It is the answer to "which part of this system is that?" — and the reason it exists as a definition is that the answer is frequently non-obvious, and getting it wrong puts code in the wrong place forever.
covers — string[], required
The boundary, written as a list of what is inside it.
The purpose of covers is decidability. Someone — a person, an agent — has a
new piece of work and needs to know whether it belongs here. covers should let
them answer that without asking you.
Which means writing it at the level of concerns, not files:
# No. This is a directory listing with extra steps.
covers:
- src/render/scene.ts
- src/render/raster.ts
- src/render/color.ts
# Yes. This tells you where a new thing goes.
covers:
- The scene graph and its traversal
- Rasterisation and caching
- Colour managementThe first version is wrong the moment somebody adds a file. The second one still decides correctly for code that does not exist yet.
How many
Three to seven, for most systems.
The test is: would you assign work to it? "Can you take a look at rendering this sprint" is a sentence people say. "Can you take a look at the colour management utility module" is not — that is a component.
Fewer than three and you have not partitioned anything. More than about eight and your domains have become components, or your concept is hiding two products.
Domains do not nest
There is no sub-domain. If you find yourself wanting one, you want either
- a broader
coverslist on the parent domain, or - a
componentinside it.
Flat is deliberate. A nested taxonomy is a thing people spend a week arguing about and then never consult, and an agent has no way to know which level is authoritative.
Examples
A desktop application
use "@psy/morphic"
export domain Rendering:
description: Turning a document into pixels.
covers:
- The scene graph and its traversal
- Rasterisation and caching
- Colour management and profile conversion
excludes:
- Anything that mutates the document. Rendering is read-only.
invariants:
- The renderer is pure: same document, same pixels
- No rendering code imports from Tools
export domain Tools:
description: Everything the user draws and edits with.
covers:
- Tool activation and modal state
- Hit testing and selection
- The command stack and undo
excludes:
- Drawing to the screen. Tools mutate the document; the renderer draws.
export domain Files:
description: Reading and writing documents.
covers:
- The on-disk format and its versioning
- Import and export of foreign formats
- Autosave and crash recovery---
name: rendering
description: "Domain: Turning a document into pixels."
---
# Rendering
## Covers
- The scene graph and its traversal
- Rasterisation and caching
- Colour management and profile conversion
## Excludes
- Anything that mutates the document. Rendering is read-only.
## Invariants
- The renderer is pure: same document, same pixels
- No rendering code imports from Tools
## Authority
This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.excludes is the property I would add first. A boundary stated only from the
inside is half a boundary — and the exclusions are where the mistakes actually
happen, because they are the plausible-looking wrong answers.
A backend platform
export domain Identity:
description: Who a request is from, and what they may do.
covers:
- Authentication and session issuance
- Roles, permissions and policy evaluation
- Service-to-service credentials
excludes:
- User profile data. That is Accounts. Identity knows a subject id and nothing else about the person.
entryPoints:
- POST /auth/token
- The `authorize(subject, action, resource)` policy call
export domain Billing:
description: Charging for usage, and everything downstream of it.
covers:
- Metering and usage aggregation
- Invoicing, payment capture and dunning
- Plan and entitlement definitions
excludes:
- Enforcing entitlements at request time. Billing decides what a customer is entitled to; Identity enforces it.
invariants:
- Money is integer minor units. Never a float, anywhere, ever.
- Every charge traces to an immutable usage recordskills/billing/SKILL.md:
---
name: billing
description: "Domain: Charging for usage, and everything downstream of it."
---
# Billing
## Covers
- Metering and usage aggregation
- Invoicing, payment capture and dunning
- Plan and entitlement definitions
## Excludes
- Enforcing entitlements at request time. Billing decides what a customer is
entitled to; Identity enforces it.
## Invariants
- Money is integer minor units. Never a float, anywhere, ever.
- Every charge traces to an immutable usage record
## Authority
This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.Read those two excludes lines together. They are doing the thing domains exist
for: drawing a line where two areas obviously touch, and saying which side each
responsibility lands on. That is a decision you make once and then re-explain
forever unless you write it down.
Extra properties that earn their place
| Property | Use it for |
|---|---|
excludes | the near-misses, and where they actually belong |
invariants | rules that hold across the whole area |
owner | the team or person, when there is one |
entryPoints | the handful of API surfaces the outside world touches |
dependencies | domains this one is allowed to depend on |
dependencies is worth a note. If your architecture has a layering rule between
domains, this is where it goes — and it is exactly the kind of thing an agent
violates by accident:
export domain Billing:
dependencies:
- Identity
- AccountsLinking components to their domain
Nothing built in does this, and you should add it. Extend component in your own
framework module so every component must name the domain it lives in:
// src/framework.psy
import { Component as MorphicComponent, Domain } from "@psy/morphic"
export abstract spec Component extends MorphicComponent as component:
abstract domain: Domainexport component LayerStack:
description: The ordered stack of layers in a document.
domain: Rendering
responsibilities:
- Maintain layer order and z-indexpsy inspect LayerStackLayerStack (src/components/LayerStack.psy)
abstract: false
keyword: component
precedence: src/components/LayerStack.psy#LayerStack > src/framework.psy#Component
description = "The ordered stack of layers in a document."
domain = Rendering
responsibilities = [
"Maintain layer order and z-index"
]domain = Rendering prints as a spec reference, not a string — that is the whole
point. Name a feature by mistake and it is PSY4001; omit it and it is
PSY3015. Your architecture map is now compiler
checked, in three lines. See
Specifying Software.
domain or component?
The one people get stuck on, from the domain side:
| domain | component | |
|---|---|---|
| Is | a region of the problem | a thing in the codebase |
| You could | assign work to it | point at it and open it |
| Contains | components | code |
| Named like | an area — Rendering, Billing | a noun — LayerStack, InvoiceBuilder |
| How many | 3–7 | as many as have non-obvious behaviour |
If a new hire would be assigned to it, it is a domain. If they would be told to go and read it, it is a component.
When not to write one
- Per folder. Folders follow domains, not the other way round. A domain that exists because a directory exists is a directory listing.
- For an area nobody works on. If no task is ever "in" it,
covershas nobody to decide for. - As a layer. "Utils", "Helpers", "Core" and "Common" are not domains. They are where things go when the domains are wrong.
How it renders
---
name: rendering
description: "Domain: Turning a document into pixels."
---
# Rendering
## Covers
- The scene graph and its traversal
- Rasterisation and caching
- Colour management and profile conversion
## Excludes
- Anything that mutates the document. Rendering is read-only.
## Invariants
- The renderer is pure: same document, same pixels
- No rendering code imports from Tools
## Authority
This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.And the skill that puts it in front of an agent:
export skill ImplementRendering:
description: Implement a change in the renderer.
when: Use when changing anything under the rendering domain.
context:
- Rendering
- LayerStack
prompt:
Keep mutation out of the renderer. If a change seems to require the
renderer to write to the document, that is a design problem — say so
rather than working around it.Next: feature