@alveolus/arch 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +11 -0
  2. package/dist/bin.mjs +4 -2
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/{cli-P5PwH9OE.mjs → docs-DcFgskuN.mjs} +214 -43
  5. package/dist/docs-DcFgskuN.mjs.map +1 -0
  6. package/dist/index.d.mts +53 -12
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +2 -2
  9. package/docs/core/application/command-handlers.md +617 -0
  10. package/docs/core/application/event-publishers.md +234 -0
  11. package/docs/core/application/event-translators.md +329 -0
  12. package/docs/core/application/index.md +99 -0
  13. package/docs/core/application/integration-events.md +277 -0
  14. package/docs/core/application/outbox.md +416 -0
  15. package/docs/core/application/query-handlers.md +292 -0
  16. package/docs/core/application/unit-of-work.md +352 -0
  17. package/docs/core/domain/aggregates.md +822 -0
  18. package/docs/core/domain/domain-errors.md +251 -0
  19. package/docs/core/domain/domain-events.md +292 -0
  20. package/docs/core/domain/domain-services.md +249 -0
  21. package/docs/core/domain/entities.md +431 -0
  22. package/docs/core/domain/index.md +93 -0
  23. package/docs/core/domain/ports.md +284 -0
  24. package/docs/core/domain/repositories.md +335 -0
  25. package/docs/core/domain/value-objects.md +425 -0
  26. package/docs/core/domain/views.md +265 -0
  27. package/docs/core/index.md +108 -0
  28. package/docs/core/strategic/anti-corruption-layers.md +349 -0
  29. package/docs/core/strategic/index.md +83 -0
  30. package/docs/core/strategic/open-host-services.md +287 -0
  31. package/docs/core/strategic/published-language.md +265 -0
  32. package/docs/core/utilities/result.md +413 -0
  33. package/docs/guide/agents.md +68 -0
  34. package/docs/guide/existing-project.md +108 -0
  35. package/docs/guide/getting-started.md +286 -0
  36. package/docs/guide/learning-path.md +123 -0
  37. package/docs/guide/project-layout.md +324 -0
  38. package/docs/guide/versioning.md +42 -0
  39. package/docs/integrations/index.md +112 -0
  40. package/docs/integrations/nestjs.md +169 -0
  41. package/docs/rules/index.md +185 -0
  42. package/docs/rules/layers/no-driving-shortcut.md +119 -0
  43. package/docs/rules/layers/no-impure-domain.md +191 -0
  44. package/docs/rules/layers/no-outward-import.md +186 -0
  45. package/docs/rules/layers/no-portless-adapter.md +123 -0
  46. package/docs/rules/strategic/no-cross-context-import.md +140 -0
  47. package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
  48. package/docs/rules/strategic/no-leaky-host-service.md +107 -0
  49. package/docs/rules/strategic/no-unmapped-context.md +114 -0
  50. package/docs/rules/tactical/no-aggregate-reference.md +139 -0
  51. package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
  52. package/docs/rules/tactical/no-foreign-query-dependency.md +201 -0
  53. package/docs/rules/tactical/no-loose-code.md +171 -0
  54. package/docs/rules/tactical/no-misplaced-class.md +146 -0
  55. package/docs/rules/tactical/no-public-field.md +113 -0
  56. package/docs/rules/tactical/no-stateful-service.md +102 -0
  57. package/docs/rules/tactical/no-thrown-failure.md +162 -0
  58. package/docs/rules/tooling/no-loose-disable.md +98 -0
  59. package/package.json +4 -3
  60. package/dist/cli-P5PwH9OE.mjs.map +0 -1
@@ -0,0 +1,185 @@
1
+ ---
2
+ description: "Architecture checks for Domain-Driven Design in TypeScript: alveolus arch check reports every way a project drifts from its layers and bounded contexts."
3
+ ---
4
+
5
+ # Rules
6
+
7
+ `alveolus arch check` applies rules that each report one way a project drifts: a shortcut between
8
+ bounded contexts, a framework leaking into the domain, a helper that lands nowhere in particular.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Command</dt><dd><code>npx alveolus arch check</code></dd>
12
+ <dt>Rule names</dt><dd><a href="#how-rules-are-named"><code>&lt;category&gt;/no-&lt;what it reports&gt;</code></a></dd>
13
+ <dt>Categories</dt><dd><a href="#strategic">Strategic</a>, <a href="#layers">Layers</a>, <a href="#tactical">Tactical</a></dd>
14
+ <dt>Default</dt><dd>Every rule on; test files never checked</dd>
15
+ <dt>Where</dt><dd><a href="#where-a-rule-applies">Every rule on a core context, the boundary rules on a supporting or generic one</a></dd>
16
+ <dt>Config</dt><dd><a href="#turn-a-rule-off"><code>rules</code></a> in <code>alveolus.config.ts</code></dd>
17
+ </dl>
18
+
19
+ ## Why
20
+
21
+ A review catches what a reviewer looks at. The import that crosses a boundary, the class in the
22
+ wrong folder or the error thrown instead of returned slip through, one change at a time, and
23
+ coding agents make more changes than anyone reviews.
24
+
25
+ ::: tip The fix
26
+ Each architecture decision becomes a rule that runs on every change. A violation says where, what
27
+ is wrong and what is allowed instead, so a developer or an agent can fix it without knowing the
28
+ whole architecture.
29
+ :::
30
+
31
+ ## How rules are named
32
+
33
+ Every rule is named `<category>/no-<what it reports>`: the category says which part of the
34
+ architecture it guards, the rest says what a violation is.
35
+
36
+ <div class="al-cards">
37
+ <div class="al-card"><span class="al-card-title"><code>strategic/</code></span>Between bounded contexts: what may cross a boundary, and through which door.</div>
38
+ <div class="al-card"><span class="al-card-title"><code>layers/</code></span>Inside a bounded context: which layer may depend on which, and what each one may import.</div>
39
+ <div class="al-card"><span class="al-card-title"><code>tactical/</code></span>Inside the domain and the application: how building blocks are written and where they live.</div>
40
+ </div>
41
+
42
+ ## Where a rule applies
43
+
44
+ `subdomains` in `alveolus.config.ts` says which bounded contexts are
45
+ [core, supporting or generic](../guide/project-layout.md#core-supporting-generic). A core context
46
+ and the shared kernel are checked by every rule. A supporting or generic context is checked only
47
+ by the rules about its boundary, the `strategic/` and `tooling/` ones: how it is written inside is
48
+ its own business. The "Applies to" line of each rule page says which it is.
49
+
50
+ ## Strategic
51
+
52
+ | Rule | Reports |
53
+ | --- | --- |
54
+ | [`strategic/no-cross-context-import`](./strategic/no-cross-context-import.md) | An import from another bounded context that is not its open host service, a composition root that re-exports. |
55
+ | [`strategic/no-fat-shared-kernel`](./strategic/no-fat-shared-kernel.md) | An aggregate, a repository or a handler in the shared kernel. |
56
+ | [`strategic/no-leaky-host-service`](./strategic/no-leaky-host-service.md) | An open host service that exposes a class of its context instead of the published language. |
57
+ | [`strategic/no-unmapped-context`](./strategic/no-unmapped-context.md) | A context consuming one the context map does not allow, or two contexts that depend on each other. |
58
+
59
+ ## Layers
60
+
61
+ In core bounded contexts and the shared kernel.
62
+
63
+ | Rule | Reports |
64
+ | --- | --- |
65
+ | [`layers/no-driving-shortcut`](./layers/no-driving-shortcut.md) | A driving adapter reaching a repository, a port or an aggregate instead of calling a handler. |
66
+ | [`layers/no-impure-domain`](./layers/no-impure-domain.md) | The domain importing a framework, a database or another layer. |
67
+ | [`layers/no-outward-import`](./layers/no-outward-import.md) | A dependency pointing away from the domain, and a file outside the layers. |
68
+ | [`layers/no-portless-adapter`](./layers/no-portless-adapter.md) | A driven adapter that extends no port, a port declared outside the domain. |
69
+
70
+ ## Tactical
71
+
72
+ In core bounded contexts and the shared kernel.
73
+
74
+ | Rule | Reports |
75
+ | --- | --- |
76
+ | [`tactical/no-aggregate-reference`](./tactical/no-aggregate-reference.md) | An aggregate holding another aggregate instead of its identifier, an entity held by two aggregates. |
77
+ | [`tactical/no-foreign-command-dependency`](./tactical/no-foreign-command-dependency.md) | A command handler receiving a query repository, another handler or a plain class. |
78
+ | [`tactical/no-foreign-query-dependency`](./tactical/no-foreign-query-dependency.md) | A query handler receiving what writes or changes state. |
79
+ | [`tactical/no-loose-code`](./tactical/no-loose-code.md) | Code outside a building block in the domain or the application: a plain or static-only class, a class that extends an expression, a function, an enum, a namespace, module state, a computed constant; anything but the module in a composition root. |
80
+ | [`tactical/no-misplaced-class`](./tactical/no-misplaced-class.md) | A class in the wrong folder or file, two classes in one file. |
81
+ | [`tactical/no-public-field`](./tactical/no-public-field.md) | A public field on an aggregate, an entity, a value object or an identifier. |
82
+ | [`tactical/no-stateful-service`](./tactical/no-stateful-service.md) | A domain service holding a port, a repository or another service. |
83
+ | [`tactical/no-thrown-failure`](./tactical/no-thrown-failure.md) | A business failure thrown instead of returned. |
84
+
85
+ ## Tooling
86
+
87
+ | Rule | Reports |
88
+ | --- | --- |
89
+ | [`tooling/no-loose-disable`](./tooling/no-loose-disable.md) | A disable comment that names no known rule, gives no reason, or disables nothing. |
90
+
91
+ ## Read a violation
92
+
93
+ Violations are grouped by file. Each one gives the line, the rule, what is wrong and what is allowed
94
+ instead:
95
+
96
+ ```
97
+ src/ordering/application/commands/place-order.command.ts
98
+ 4 error layers/no-outward-import: The application layer imports
99
+ src/ordering/driven/pg/adapters/mailer.adapter.ts (ordering driven):
100
+ it may only import domain, application, published-language.
101
+ ```
102
+
103
+ `--format json` gives the same information as JSON, with the symbol involved, for tools and
104
+ agents.
105
+
106
+ ## Building blocks are recognised by inheritance
107
+
108
+ The rules know what a class is from what it extends: `class Order extends AggregateRoot` is an
109
+ aggregate, wherever it is and whatever its name. A class that extends one of your own base classes
110
+ counts too, as long as that base class extends a building block of `@alveolus/core`.
111
+
112
+ ::: tip
113
+ There are no decorators or naming conventions to learn: the class says what it is, and the rules
114
+ take it at its word.
115
+ :::
116
+
117
+ ## Every import counts
118
+
119
+ The rules that check imports read every way a file can depend on another one, not only
120
+ `import … from`:
121
+
122
+ ```ts
123
+ import { Pool } from "pg";
124
+ export { Pool } from "pg";
125
+ type Pool = import("pg").Pool;
126
+ const pg = await import("pg");
127
+ const pg = require("pg");
128
+ import pg = require("pg");
129
+ ```
130
+
131
+ A global declared by the project, in a `declare global` block, counts as an import of the file that
132
+ declares it.
133
+
134
+ An import the analysis cannot see through counts as a file outside the project: one that does not
135
+ resolve, such as a `.js` file without types, one whose path is computed at runtime, or one that is
136
+ ignored, such as a test file. No layer imports it: only the composition root and the files at the
137
+ root of `src/` may.
138
+
139
+ ```
140
+ src/ordering/domain/services/pricing.service.ts
141
+ 2 error layers/no-impure-domain: The domain imports
142
+ src/ordering/domain/services/db.spec.ts (ignored by the analysis):
143
+ it may only import the domain.
144
+ ```
145
+
146
+ ## Set the level of a rule
147
+
148
+ Every rule reports an `error` by default, and an error fails the check. In `alveolus.config.ts`,
149
+ lower a rule to `warn` or `info`, which report without failing, or turn it `off`, with its full
150
+ name:
151
+
152
+ ```ts [alveolus.config.ts]
153
+ export default defineConfig({
154
+ boundedContexts: { ordering: "ordering" },
155
+ contextMap: { ordering: { consumes: [] } },
156
+ root: "src",
157
+ rules: { "tactical/no-misplaced-class": "off", "tactical/no-public-field": "warn" },
158
+ subdomains: { core: ["ordering"] },
159
+ });
160
+ ```
161
+
162
+ `warn` and `info` are the way in on an existing project: a rule reports for a while, the team
163
+ fixes, then it becomes an error.
164
+
165
+ To turn one violation off where it stands, with a reason, write a disable comment above the line:
166
+ see [Getting started](../guide/getting-started.md#turn-a-violation-off). To adopt the rules on an
167
+ existing project without turning them off, record the current violations in a baseline: see
168
+ [Getting started](../guide/getting-started.md#adopt-it-on-an-existing-project).
169
+
170
+ Tests and their companions (`*.spec.ts`, `*.test.ts`, `*.e2e-spec.ts`, `*.fixture.ts`, `*.stories.ts`,
171
+ `__tests__/`, `__mocks__/`) are never checked, and production code may not
172
+ import them.
173
+
174
+ ## What the rules cannot see
175
+
176
+ The rules read the code, not what it does at run time: a port whose adapter reads the views, an
177
+ interface shaped like an aggregate, or an anti-corruption layer that passes data through untouched
178
+ all look right. Each rule page lists its limits in a **Limits** section, with what to watch for in
179
+ review. A file that matches `ignore` in `alveolus.config.ts` is not analysed at all: review a
180
+ change to `ignore` as you would review a rule turned off.
181
+
182
+ ## See also
183
+
184
+ - [Getting started](../guide/getting-started.md), to configure and run the checks
185
+ - [Project layout](../guide/project-layout.md), the layout the rules keep
@@ -0,0 +1,119 @@
1
+ ---
2
+ description: "Architecture rule: a driving adapter calls the command and query handlers, and never reaches a repository, a port or an aggregate of the domain itself."
3
+ ---
4
+
5
+ # no-driving-shortcut
6
+
7
+ A driving adapter calls the command and query handlers: it never reaches a repository, a port, an
8
+ aggregate or a domain service itself.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Rule</dt><dd><code>layers/no-driving-shortcut</code></dd>
12
+ <dt>Category</dt><dd><a href="/rules/#layers">Layers</a>: what each layer may depend on</dd>
13
+ <dt>Reports</dt><dd>A file of <code>driving/</code> that imports a <code>CommandRepository</code>, a <code>QueryRepository</code>, a <code>Port</code>, an <code>AggregateRoot</code>, an <code>Entity</code> or a <code>DomainService</code></dd>
14
+ <dt>Applies to</dt><dd>Every file in <code>driving/</code>, in every core bounded context and the shared kernel</dd>
15
+ <dt>Turn off</dt><dd><a href="#turn-it-off"><code>"layers/no-driving-shortcut": "off"</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ `OrdersController` injects `Orders`, loads the order, calls `place()` and saves it: the use case
21
+ now lives in the controller. The next entry point, a message consumer, copies those lines; the
22
+ transaction, the outbox and the events that `PlaceOrderHandler` handles are skipped, and the
23
+ rules that `no-foreign-command-dependency` keeps on handlers do not apply to a controller.
24
+
25
+ ::: tip The fix
26
+ A driving adapter translates a request into a command or a query, calls its handler, and
27
+ translates the `Result` into a response. The use case exists once, in the application.
28
+ :::
29
+
30
+ ## What it checks
31
+
32
+ Every import of a file in `driving/` that points to a file of the project:
33
+
34
+ | Imported class | Allowed |
35
+ | --- | --- |
36
+ | A `CommandHandler`, a `QueryHandler`, an `EventTranslator` | ✅ |
37
+ | A value object, an identifier, a domain error, a view, a representation | ✅ |
38
+ | A `CommandRepository`, a `QueryRepository`, a `Port` | ❌ |
39
+ | An `AggregateRoot`, an `Entity`, a `DomainService` | ❌ |
40
+
41
+ A class counts by what it extends: `Orders` extends `CommandRepository<Order>`. `import type`
42
+ counts too, since a dependency injected by type is a dependency. Every form of import counts, see
43
+ [Every import counts](../index.md#every-import-counts).
44
+
45
+ ## What it reports
46
+
47
+ ```
48
+ src/ordering/driving/http/orders.controller.ts
49
+ 3 error layers/no-driving-shortcut: Imports Orders, a CommandRepository:
50
+ a driving adapter calls the command and query handlers, never
51
+ the ports, repositories or aggregates of the domain.
52
+ ```
53
+
54
+ ## Fix it
55
+
56
+ ### Call the handler
57
+
58
+ So that the use case exists once, the controller receives the handler and passes it a command.
59
+
60
+ <div class="al-compare">
61
+
62
+ ```ts [❌ Avoid: src/ordering/driving/http/orders.controller.ts]
63
+ export class OrdersController {
64
+ constructor(private readonly orders: Orders) {}
65
+
66
+ async place(id: string): Promise<void> {
67
+ const order = await this.orders.findById(new OrderId(id));
68
+ order?.place();
69
+ await this.orders.save(order);
70
+ }
71
+ }
72
+ ```
73
+
74
+ ```ts [✅ Prefer: src/ordering/driving/http/orders.controller.ts]
75
+ export class OrdersController {
76
+ constructor(private readonly placeOrder: PlaceOrderHandler) {}
77
+
78
+ async place(id: string): Promise<void> {
79
+ const result = await this.placeOrder.handle({ orderId: id });
80
+ if (!result.ok) {
81
+ throw new BadRequestException(result.error.type);
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ </div>
88
+
89
+ ### Read through a query
90
+
91
+ So that a read goes through the same door, a controller that needs data calls a
92
+ [query handler](../../core/application/query-handlers.md), never a query repository.
93
+
94
+ ## Limits
95
+
96
+ ::: warning What the rule cannot see
97
+ - A handler injected and then bypassed: the controller may still receive a repository through a
98
+ framework token (`@Inject("ORDERS")`) typed as `unknown`. In review, a driving adapter has no
99
+ provider but handlers.
100
+ - What the driving adapter does with a value object or an error: those stay importable to map
101
+ requests and responses.
102
+ :::
103
+
104
+ ## Turn it off
105
+
106
+ ```ts [alveolus.config.ts]
107
+ rules: { "layers/no-driving-shortcut": "off" },
108
+ ```
109
+
110
+ On an existing project, prefer a [baseline](../../guide/getting-started.md#adopt-it-on-an-existing-project):
111
+ new entry points go through the handlers while you move the old use cases out of the controllers.
112
+
113
+ ## See also
114
+
115
+ - [Command handlers](../../core/application/command-handlers.md) and
116
+ [Query handlers](../../core/application/query-handlers.md), what a driving adapter calls
117
+ - [`layers/no-outward-import`](./no-outward-import.md), what each layer may import
118
+ - [Project layout: layers](../../guide/project-layout.md#layers)
119
+ - [Rules](../index.md), every rule by category
@@ -0,0 +1,191 @@
1
+ ---
2
+ description: "Architecture rule: the domain layer depends on nothing but itself and @alveolus/core, with no ORM, framework or infrastructure import."
3
+ ---
4
+
5
+ # no-impure-domain
6
+
7
+ The domain depends on nothing but itself: its own domain, the domain of the shared kernel and the
8
+ domain building blocks of `@alveolus/core`.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Rule</dt><dd><code>layers/no-impure-domain</code></dd>
12
+ <dt>Category</dt><dd><a href="/rules/#layers">Layers</a>: what each layer may depend on</dd>
13
+ <dt>Reports</dt><dd>The domain importing a framework, a database, another layer or a package not allowed, using a global of the host, reading the clock or drawing a random value</dd>
14
+ <dt>Applies to</dt><dd>Every file in <code>domain/</code>, in every core bounded context and the shared kernel</dd>
15
+ <dt>Turn off</dt><dd><a href="#turn-it-off"><code>"layers/no-impure-domain": "off"</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ The `Order` aggregate carries TypeORM decorators so that it can be saved as is, and calls a mailer
21
+ when it is placed. Upgrading the ORM now means touching the business rules, and testing that an
22
+ empty order is refused needs a database and an SMTP server.
23
+
24
+ ::: tip The fix
25
+ The domain imports nothing technical. Storage and email are [ports](../../core/domain/ports.md)
26
+ declared by the domain and implemented by driven adapters. The business rules change when the
27
+ business does, not when a library does, and run in a test without any infrastructure.
28
+ :::
29
+
30
+ ## What it checks
31
+
32
+ Every import of a file in `domain/`, in a bounded context or in the shared kernel:
33
+
34
+ | Import | Allowed when |
35
+ | --- | --- |
36
+ | A file of the project | It is in the `domain/` of the same context or of the shared kernel. |
37
+ | `@alveolus/core` | Every imported name is a domain building block or part of `Result`: `AggregateRoot`, `Entity`, `ValueObject`, `Identifier`, `DomainEvent`, `DomainError`, `DomainService`, `Port`, `Clock`, `IdGenerator`, `CommandRepository`, `QueryRepository`, `View`, `Result`, `ok`, `err`, `map`, `mapErr`, `andThen`, `combine` and their `Any…` types. |
38
+ | Any other package | It is listed in `domainDependencies`; when its entry lists names, every imported name is one of them. |
39
+
40
+ Importing from the root `@alveolus/core` is fine: the rule checks each imported name, not the path.
41
+
42
+ Every form of import counts, see [Every import counts](../index.md#every-import-counts).
43
+
44
+ ### Globals
45
+
46
+ A global is used without an import, so the rule reads every global the domain uses:
47
+
48
+ | Global | Allowed when |
49
+ | --- | --- |
50
+ | An ECMAScript built-in: `Array`, `Map`, `JSON`, `Math`, `Promise`, `Intl`… | Always, except `Date.now()`, `new Date()` without argument, `Date()` and `Math.random()`, which read the clock or draw a random value. |
51
+ | A global of the host: `fetch`, `process`, `console`, `setTimeout`, `crypto`, a DOM type such as `Response`… | Never. |
52
+ | A global declared by the project, in a `declare global` block | As if the domain imported the file that declares it. |
53
+
54
+ The rule tells them apart by where they are declared: the ECMAScript library of TypeScript, the
55
+ types of the host (DOM, Node), or a file of the project. A local variable named `fetch` is not a
56
+ global.
57
+
58
+ ## What it reports
59
+
60
+ ```
61
+ src/ordering/domain/aggregates/order.aggregate.ts
62
+ 1 error layers/no-impure-domain: The domain imports @nestjs/common:
63
+ add it to domainDependencies if the domain really needs it.
64
+ 2 error layers/no-impure-domain: The domain imports UnitOfWork from
65
+ @alveolus/core: only domain building blocks and Result are allowed.
66
+ 5 error layers/no-impure-domain: The domain imports
67
+ src/ordering/driven/smtp/adapters/mailer.adapter.ts
68
+ (ordering driven): it may only import the domain.
69
+ ```
70
+
71
+ A global of the host, the clock and randomness:
72
+
73
+ ```
74
+ src/ordering/domain/services/pricing.service.ts
75
+ 3 error layers/no-impure-domain: The domain uses fetch, a global of the
76
+ host: reach it through a port.
77
+
78
+ src/ordering/domain/aggregates/order.aggregate.ts
79
+ 12 error layers/no-impure-domain: The domain reads the system clock with
80
+ Date.now: receive the time from the Clock port.
81
+ 13 error layers/no-impure-domain: The domain draws a random value with
82
+ Math.random: receive it from a port, such as IdGenerator.
83
+ ```
84
+
85
+ A name not allowed from a restricted package is reported as well:
86
+
87
+ ```
88
+ layers/no-impure-domain: The domain imports format from date-fns:
89
+ domainDependencies only allows addDays, isBefore.
90
+ ```
91
+
92
+ ## Fix it
93
+
94
+ ### Keep infrastructure behind a port
95
+
96
+ So that the business rules survive a change of database, framework or mail provider, the domain
97
+ declares what it needs as a port, and a driven adapter implements it. Transactions belong to the
98
+ command handler, not to the domain.
99
+
100
+ <div class="al-compare">
101
+
102
+ ```ts [❌ Avoid: src/ordering/domain/aggregates/order.aggregate.ts]
103
+ import { Injectable } from "@nestjs/common";
104
+ import { AggregateRoot, UnitOfWork } from "@alveolus/core";
105
+ import { Column, Entity } from "typeorm";
106
+
107
+ import { Mailer } from "../../driven/smtp/adapters/mailer.adapter";
108
+ ```
109
+
110
+ ```ts [✅ Prefer: src/ordering/domain/aggregates/order.aggregate.ts]
111
+ import { AggregateRoot, err, ok, type Result } from "@alveolus/core";
112
+
113
+ import { Money } from "../../../shared-kernel/domain/value-objects/money.value-object";
114
+ import { InvalidTotal } from "../errors/invalid-total.error";
115
+ ```
116
+
117
+ </div>
118
+
119
+ ### Receive the time and random values
120
+
121
+ So that a rule about dates gives the same answer in a test as in production, the domain never
122
+ reads the clock or draws a random value itself. The command handler asks the
123
+ [`Clock` and `IdGenerator` ports](../../core/domain/ports.md) and passes the values in.
124
+
125
+ <div class="al-compare">
126
+
127
+ ```ts [❌ Avoid: src/ordering/domain/aggregates/order.aggregate.ts]
128
+ public place(): Result<void, never> {
129
+ this.placedAt = new Date();
130
+ return ok(undefined);
131
+ }
132
+ ```
133
+
134
+ ```ts [✅ Prefer: src/ordering/domain/aggregates/order.aggregate.ts]
135
+ public place(at: Date): Result<void, never> {
136
+ this.placedAt = at;
137
+ return ok(undefined);
138
+ }
139
+ ```
140
+
141
+ </div>
142
+
143
+ ### Map storage outside the domain
144
+
145
+ So that the aggregate is not shaped by its table, it exposes a snapshot, and the repository
146
+ adapter maps that snapshot to its storage. See [Aggregates](../../core/domain/aggregates.md).
147
+
148
+ ## Allow a package
149
+
150
+ Some packages belong in a domain, such as a decimal library for money. Declare them, with `true`
151
+ to allow everything they export, or with the names you allow:
152
+
153
+ ```ts [alveolus.config.ts]
154
+ export default defineConfig({
155
+ boundedContexts: { ordering: "ordering" },
156
+ contextMap: { ordering: { consumes: [] } },
157
+ domainDependencies: {
158
+ "date-fns": ["addDays", "isBefore"],
159
+ "decimal.js": true,
160
+ },
161
+ root: "src",
162
+ subdomains: { core: ["ordering"] },
163
+ });
164
+ ```
165
+
166
+ The packages of `domainDependencies` are allowed in the application too.
167
+
168
+ ## Limits
169
+
170
+ ::: warning What the rule cannot see
171
+ - `domainDependencies` is not transitive: a package you allow may import anything itself. Allow
172
+ small, pure packages, such as a decimal or a date library.
173
+ - A file that matches `ignore` in `alveolus.config.ts` is not analysed at all, and the domain may
174
+ import it: review a change to `ignore` as you would review a rule turned off.
175
+ :::
176
+
177
+ ## Turn it off
178
+
179
+ ```ts [alveolus.config.ts]
180
+ rules: { "layers/no-impure-domain": "off" },
181
+ ```
182
+
183
+ On an existing project, prefer a [baseline](../../guide/getting-started.md#adopt-it-on-an-existing-project):
184
+ new code keeps the domain pure while you clean up the old one.
185
+
186
+ ## See also
187
+
188
+ - [Project layout: layers](../../guide/project-layout.md#layers)
189
+ - [Ports](../../core/domain/ports.md), to reach infrastructure from the domain
190
+ - [`layers/no-outward-import`](./no-outward-import.md), the same idea for the other layers
191
+ - [Rules](../index.md), every rule by category