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).