Purpose¶
The domain layer of a context should hold only recognized building blocks — entities, value_objects, events, services, errors — never loose utility folders. But a large context often groups those components under a subdomain (shipping/entities, shipping/value_objects). This rule restricts the layer to the allowed component names while letting a single subdomain grouping tier through, so real subdomains pass and stray directories do not.
Configuration¶
rules:
- name: domain-components
type: allowed-children
description: The domain layer holds only recognized component directories
within: src/contexts/{ctx}/domain
kind: dir
group_level: optional
allow: [entities, value_objects, events, services, errors]
ignore: [__init__.py]
within matches each context's domain directory; kind: dir inspects only its subdirectories. A child whose name is in allow passes as a component. With group_level: optional, a child directory that is not an allowed name is treated as a one-level grouping tier (a subdomain) and passes only if it holds at least one child and every one of those children is an allowed component. Grouping does not nest, so a group whose child is itself an unknown directory fails. ignore skips __init__.py.
Violation Example¶
The orders domain adds two directories that are neither components nor valid grouping tiers — a helpers/ folder holding a non-component subdirectory, and a refunds/ subdomain that holds no components yet:
src/contexts/orders/domain/
├── __init__.py
├── entities/
│ └── order.py
├── value_objects/
│ └── money.py
├── errors/
│ └── order_not_found_error.py
├── helpers/ # a would-be grouping tier...
│ └── serialization/ # ...whose child is not a component
│ └── json.py
└── refunds/ # a grouping tier that holds no components
└── __init__.py
helpers/ is treated as a grouping tier, but serialization/ is not an allowed component, so it fails. refunds/ holds only __init__.py (skipped by ignore), leaving no sub-components, so it cannot pass as a grouping tier either.
Passing Example¶
Drop the loose helpers/ folder, and give the subdomain real component children so it reads as a genuine grouping tier:
src/contexts/orders/domain/
├── __init__.py
├── entities/
│ └── order.py
├── value_objects/
│ └── money.py
├── errors/
│ └── order_not_found_error.py
└── shipping/ # a real subdomain grouping tier
├── entities/
│ └── shipment.py
└── value_objects/
└── address.py
shipping/ is not an allowed name, but every child it holds (entities, value_objects) is an allowed component, so it passes as a subdomain.
Output¶
$ psl check
src/contexts/orders/domain/helpers/serialization
[domain-components] The domain layer holds only recognized component directories
unexpected 'serialization' in grouping directory 'helpers'
src/contexts/orders/domain/refunds
[domain-components] The domain layer holds only recognized component directories
'refunds' is neither an allowed component nor a grouping directory (holds no sub-components)
Found 2 violation(s).