@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,186 @@
1
+ ---
2
+ description: "Architecture rule: every dependency points towards the domain, as in hexagonal and clean architecture, and only the composition root sees every layer."
3
+ ---
4
+
5
+ # no-outward-import
6
+
7
+ Every dependency points towards the domain, only the composition root sees every layer, and every
8
+ file belongs to a layer.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Rule</dt><dd><code>layers/no-outward-import</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>An import that points away from the domain, a package the application may not use, a file outside the layers or in the wrong folder of its layer</dd>
14
+ <dt>Applies to</dt><dd><code>application/</code>, <code>published-language/</code>, <code>driven/</code>, <code>driving/</code>, composition roots and files at the root of <code>src/</code>; in core bounded contexts and the shared kernel</dd>
15
+ <dt>Turn off</dt><dd><a href="#turn-it-off"><code>"layers/no-outward-import": "off"</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ The `PlaceOrderHandler` imports `PgOrders` to save the order, and a controller imports the mailer
21
+ adapter to send a confirmation. Replacing the database now means rewriting use cases, and a
22
+ controller has started sending emails.
23
+
24
+ ::: tip The fix
25
+ Every arrow points inwards. The application knows the domain and its ports, never an adapter; each
26
+ adapter knows the application, never the adapters on the other side. The composition root is the
27
+ one place that knows how everything fits, so each layer can be replaced, tested and read on its
28
+ own.
29
+ :::
30
+
31
+ ## What it checks
32
+
33
+ ### Imports between layers
34
+
35
+ Within the same context, or towards the shared kernel:
36
+
37
+ | From | May import |
38
+ | --- | --- |
39
+ | `application/` | `domain/`, `application/`, its own `published-language/`. Packages: `@alveolus/core`, `domainDependencies`, `applicationDependencies`. |
40
+ | `published-language/` | Its own `published-language/`. From `@alveolus/core`, only `PublishedLanguage`, `IntegrationEvent` and `JsonValue`; other packages, such as a schema library, are fine. |
41
+ | `driven/` | `domain/`, `application/`, `published-language/`, `driven/`, any package. |
42
+ | `driving/` | `domain/`, `application/`, `published-language/`, `driving/`, any package. |
43
+ | the composition root | Anything in its context and in the shared kernel. |
44
+ | files at the root of `src/` | Composition roots, and each other. |
45
+
46
+ No layer imports a composition root. The domain has its own rule,
47
+ [`layers/no-impure-domain`](./no-impure-domain.md); imports from another context are checked by
48
+ [`strategic/no-cross-context-import`](../strategic/no-cross-context-import.md).
49
+
50
+ Every form of import counts, see [Every import counts](../index.md#every-import-counts).
51
+
52
+ ### Files outside the layers
53
+
54
+ <div class="al-cards al-cards-2">
55
+ <div class="al-card"><span class="al-card-title">Inside a context</span>A file directly in a context that is not its composition root belongs to no layer. A context, or a feature of the shared kernel, has a single composition root.</div>
56
+ <div class="al-card"><span class="al-card-title">Outside every context</span>A file under <code>src/</code>, outside every declared context and the shared kernel, and not at the root.</div>
57
+ </div>
58
+
59
+ The layer is the first folder of a context, or the second one in a feature of the shared kernel:
60
+ `ordering/legacy/domain/` is no layer.
61
+
62
+ ### Folders inside a layer
63
+
64
+ Each layer expects the folders of the [project layout](../../guide/project-layout.md#the-tree).
65
+ A file in another folder keeps its layer for every other rule, and is reported here. A folder of
66
+ your own goes in `layout.extraFolders` of `alveolus.config.ts`: `{ domain: ["specifications"] }`.
67
+
68
+ | Layer | Expected | Reported |
69
+ | --- | --- | --- |
70
+ | `domain/` | One folder of a kind: `aggregates/`, `entities/`, `value-objects/`, `events/`, `errors/`, `services/`, `repositories/`, `ports/`, `views/` | A file directly in `domain/`, a deeper folder, another folder name |
71
+ | `application/` | One folder of a kind: `commands/`, `queries/`, `translators/` | The same |
72
+ | `published-language/` | The files directly | Any folder |
73
+ | `driven/` | `<technology>/<folder>/`, such as `pg/adapters/` | A file without a technology, a deeper folder |
74
+ | `driving/` | `<technology>/`, any folders below, such as `http/controllers/` | A file without a technology |
75
+
76
+ ## What it reports
77
+
78
+ ```
79
+ src/ordering/application/commands/place-order.command.ts
80
+ 1 error layers/no-outward-import: The application imports
81
+ @nestjs/common: add it to applicationDependencies if the
82
+ application really needs it.
83
+ 3 error layers/no-outward-import: The application layer imports
84
+ src/ordering/driven/pg/adapters/pg-orders.adapter.ts
85
+ (ordering driven): it may only import domain, application,
86
+ published-language.
87
+
88
+ src/ordering/helpers.ts
89
+ 1 error layers/no-outward-import: The file is outside the layers:
90
+ move it to domain/, application/, published-language/,
91
+ driven/ or driving/.
92
+
93
+ src/ordering/domain/legacy/v1/aggregates/order.aggregate.ts
94
+ 1 error layers/no-outward-import: The file is nested too deep: domain/
95
+ holds one folder per kind, such as domain/aggregates/.
96
+ ```
97
+
98
+ The rule also reports:
99
+
100
+ | Case | Message |
101
+ | --- | --- |
102
+ | A layer imports a composition root | `Imports … : only the composition root wires the layers.` |
103
+ | A file at the root imports inside a context | `Files at the root import composition roots only, not ….` |
104
+ | Two composition roots in a context | `ordering has 2 composition roots (ordering.module.ts, pricing.module.ts): keep one, and move the rest into the layers.` |
105
+ | The published language imports another name from core | `The published language imports … from @alveolus/core: only published-language types are allowed.` |
106
+ | A name not allowed from a restricted package | `The application imports Controller from @nestjs/common: applicationDependencies only allows Injectable.` |
107
+ | A file outside the declared contexts | `The file is outside the bounded contexts and the shared kernel declared in alveolus.config.ts: move it, or add it to ignore.` |
108
+ | A file directly in `domain/` or `application/` | `The file sits directly in domain/: put it in the folder of its kind, such as domain/aggregates/.` |
109
+ | A folder that is no kind | `domain/helpers/ is no folder of the domain: use aggregates/, entities/, …` |
110
+ | An adapter without a technology | `The file is not under a technology: driven/ holds driven/<technology>/<folder>/, such as driven/pg/adapters/.` |
111
+
112
+ ## Fix it
113
+
114
+ ### Depend on the port, not on the adapter
115
+
116
+ So that replacing the database touches no use case, the handler receives the repository it
117
+ extends from the domain, and the composition root passes it the adapter.
118
+
119
+ <div class="al-compare">
120
+
121
+ ```ts [❌ Avoid: src/ordering/application/commands/place-order.command.ts]
122
+ import { Controller } from "@nestjs/common";
123
+
124
+ import { PgOrders } from "../../driven/pg/adapters/pg-orders.adapter";
125
+ ```
126
+
127
+ ```ts [✅ Prefer: src/ordering/application/commands/place-order.command.ts]
128
+ import { CommandHandler } from "@alveolus/core";
129
+
130
+ import { Orders } from "../../domain/repositories/orders.repository";
131
+ ```
132
+
133
+ </div>
134
+
135
+ ### Give every file a layer and a folder
136
+
137
+ So that every file has a known place, move a helper into the layer that uses it, as a building
138
+ block, in the folder of its kind. A barrel such as `domain/index.ts` is not needed: import each
139
+ file from its folder. A file that has nothing to do with the architecture, such as a script, can be left out with
140
+ `ignore` in `alveolus.config.ts`.
141
+
142
+ ## Allow a package
143
+
144
+ The application imports no framework: the composition root builds its classes. A package it really
145
+ needs is declared, with `true` to allow everything it exports, or with the names you allow:
146
+
147
+ ```ts [alveolus.config.ts]
148
+ export default defineConfig({
149
+ applicationDependencies: {
150
+ "@nestjs/common": ["Injectable"],
151
+ zod: true,
152
+ },
153
+ boundedContexts: { ordering: "ordering" },
154
+ contextMap: { ordering: { consumes: [] } },
155
+ root: "src",
156
+ subdomains: { core: ["ordering"] },
157
+ });
158
+ ```
159
+
160
+ The packages of `domainDependencies` are allowed in the application too.
161
+
162
+ ## Limits
163
+
164
+ ::: warning What the rule cannot see
165
+ - Below its technology, an adapter layer may hold any folder: `driving/http/controllers/v1/` is
166
+ fine. What those folders contain is checked by
167
+ [`tactical/no-loose-code`](../tactical/no-loose-code.md): classes only.
168
+ - A file that matches `ignore` in `alveolus.config.ts` is not analysed at all. Review a change to
169
+ `ignore` as you would review a rule turned off.
170
+ :::
171
+
172
+ ## Turn it off
173
+
174
+ ```ts [alveolus.config.ts]
175
+ rules: { "layers/no-outward-import": "off" },
176
+ ```
177
+
178
+ On an existing project, prefer a [baseline](../../guide/getting-started.md#adopt-it-on-an-existing-project):
179
+ new code keeps the arrows inwards while you fix the old ones.
180
+
181
+ ## See also
182
+
183
+ - [Project layout: who may import what](../../guide/project-layout.md#who-may-import-what)
184
+ - [Integrations](../../integrations/index.md), to build the classes in the composition root
185
+ - [`layers/no-impure-domain`](./no-impure-domain.md), the same idea for the domain
186
+ - [Rules](../index.md), every rule by category
@@ -0,0 +1,123 @@
1
+ ---
2
+ description: "Architecture rule: every driven adapter implements a port declared by the domain, as hexagonal architecture requires."
3
+ ---
4
+
5
+ # no-portless-adapter
6
+
7
+ A driven adapter exists to implement a port: every class in `driven/<technology>/adapters/`
8
+ extends one, and every port is declared by the domain.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Rule</dt><dd><code>layers/no-portless-adapter</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 driven adapter that extends no port, a port declared outside the domain</dd>
14
+ <dt>Applies to</dt><dd>Every class in <code>driven/**/adapters/</code>, and every abstract class that extends <code>Port</code>, in core bounded contexts and the shared kernel</dd>
15
+ <dt>Turn off</dt><dd><a href="#turn-it-off"><code>"layers/no-portless-adapter": "off"</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ A `Mailer` class in `driven/smtp/adapters/` sends emails, and the handler calls it directly. The
21
+ application now depends on SMTP: testing a use case sends mail, and moving to an email API means
22
+ rewriting the handler.
23
+
24
+ ::: tip The fix
25
+ The domain says what it needs in its own words, as a [port](../../core/domain/ports.md); the
26
+ adapter extends that port. The handler only knows the port, so the adapter can be swapped in a
27
+ test or when the technology changes.
28
+ :::
29
+
30
+ ## What it checks
31
+
32
+ <div class="al-cards al-cards-2">
33
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Every adapter extends a port</span>Every class in a <code>driven/**/adapters/</code> folder extends a <code>Port</code>, directly or through a repository, <code>Outbox</code>, <code>EventPublisher</code>, <code>UnitOfWork</code>, <code>Clock</code> or <code>IdGenerator</code>.</div>
34
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Every port lives in the domain</span>Every abstract class that extends <code>Port</code> is declared in <code>domain/ports/</code> or <code>domain/repositories/</code>.</div>
35
+ </div>
36
+
37
+ Abstract base classes for your adapters may also live in `driven/**/adapters/`, as long as they
38
+ extend a port.
39
+
40
+ ## What it reports
41
+
42
+ ```
43
+ src/ordering/driven/smtp/adapters/mailer.adapter.ts
44
+ 1 error layers/no-portless-adapter: Mailer is a driven adapter but
45
+ extends no Port: extend the port it implements.
46
+
47
+ src/ordering/application/commands/notifications.ts
48
+ 3 error layers/no-portless-adapter: The port Notifications is declared
49
+ outside the domain: move it to domain/ports/ or
50
+ domain/repositories/.
51
+ ```
52
+
53
+ ## Fix it
54
+
55
+ ### Extend the port the adapter implements
56
+
57
+ So that the application depends on what it needs and not on the technology, declare a port in the
58
+ domain and make the adapter extend it. Name the adapter after its technology and its port.
59
+
60
+ <div class="al-compare">
61
+
62
+ ```ts [❌ Avoid: src/ordering/driven/smtp/adapters/mailer.adapter.ts]
63
+ export class Mailer {
64
+ async send(to: string, body: string): Promise<void> {}
65
+ }
66
+ ```
67
+
68
+ ```ts [✅ Prefer: src/ordering/driven/smtp/adapters/smtp-notifications.adapter.ts]
69
+ import { Notifications } from "../../../domain/ports/notifications.port";
70
+
71
+ export class SmtpNotifications extends Notifications {
72
+ async orderPlaced(email: string): Promise<void> {}
73
+ }
74
+ ```
75
+
76
+ </div>
77
+
78
+ ### Declare ports in the domain
79
+
80
+ So that the domain knows every dependency it relies on, a port declared in the application or
81
+ next to an adapter moves to `domain/ports/`, or to `domain/repositories/` for a repository.
82
+
83
+ <div class="al-compare">
84
+
85
+ ```ts [❌ Avoid: src/ordering/application/commands/notifications.ts]
86
+ export abstract class Notifications extends Port {
87
+ abstract orderPlaced(email: string): Promise<void>;
88
+ }
89
+ ```
90
+
91
+ ```ts [✅ Prefer: src/ordering/domain/ports/notifications.port.ts]
92
+ export abstract class Notifications extends Port {
93
+ abstract orderPlaced(email: string): Promise<void>;
94
+ }
95
+ ```
96
+
97
+ </div>
98
+
99
+ ## Limits
100
+
101
+ ::: warning What the rule cannot see
102
+ - A class in `driven/<technology>/` outside `adapters/`, such as a mapper or an ORM entity: it is
103
+ not an adapter, so it needs no port, and nothing checks what it talks to.
104
+ - A port written as an interface: a port is an abstract class that extends `Port`, and an adapter
105
+ that `implements` an interface is reported as extending no port.
106
+ :::
107
+
108
+ ## Turn it off
109
+
110
+ ```ts [alveolus.config.ts]
111
+ rules: { "layers/no-portless-adapter": "off" },
112
+ ```
113
+
114
+ On an existing project, prefer a [baseline](../../guide/getting-started.md#adopt-it-on-an-existing-project):
115
+ new adapters extend a port while you wrap the old ones.
116
+
117
+ ## See also
118
+
119
+ - [Ports](../../core/domain/ports.md), what an adapter implements
120
+ - [Project layout: layers](../../guide/project-layout.md#layers)
121
+ - [`tactical/no-misplaced-class`](../tactical/no-misplaced-class.md), for the folder and file name
122
+ of an adapter
123
+ - [Rules](../index.md), every rule by category
@@ -0,0 +1,140 @@
1
+ ---
2
+ description: "Architecture rule: a bounded context is reached only through its open host service, from an anti-corruption layer or its composition root."
3
+ ---
4
+
5
+ # no-cross-context-import
6
+
7
+ A bounded context is closed: another context reaches it only through its open host service, from
8
+ an anti-corruption layer or its composition root.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Rule</dt><dd><code>strategic/no-cross-context-import</code></dd>
12
+ <dt>Category</dt><dd><a href="/rules/#strategic">Strategic</a>: what crosses a bounded context</dd>
13
+ <dt>Reports</dt><dd>An import from another bounded context that is not its open host service, used where it may be</dd>
14
+ <dt>Applies to</dt><dd>Every file of every bounded context, whatever its subdomain, and of the shared kernel</dd>
15
+ <dt>Turn off</dt><dd><a href="#turn-it-off"><code>"strategic/no-cross-context-import": "off"</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ An adapter of ordering imports `Product` from the catalog domain, because it had the fields it
21
+ needed. The two contexts now share one model without anyone deciding it: renaming a field in the
22
+ catalog breaks ordering, and nothing showed the dependency until it broke.
23
+
24
+ ::: tip The fix
25
+ One door on each side. The catalog exposes an
26
+ [open host service](../../core/strategic/open-host-services.md); ordering calls it from an
27
+ [anti-corruption layer](../../core/strategic/anti-corruption-layers.md) that translates the answer
28
+ into its own model. Everything in between is free to change.
29
+ :::
30
+
31
+ ## What it checks
32
+
33
+ When a file of one bounded context imports a file of another one:
34
+
35
+ <div class="al-cards al-cards-2">
36
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Only open host services</span>Every imported name is a class that implements <code>OpenHostService</code>.</div>
37
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Only from an anti-corruption layer</span>In a core context, the importing file declares a class that implements <code>AntiCorruptionLayer</code>, or is the composition root of its context. A <a href="../../guide/project-layout.md#core-supporting-generic">supporting or generic context</a> calls the service from anywhere: it has no model to protect.</div>
38
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Composition roots meet freely</span>A composition root may import another context's composition root, to reach its open host services. It re-exports nothing, so that no other context reaches its model through it.</div>
39
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span>No foreign published language</span>The published language of another context is never imported, not even its types.</div>
40
+ </div>
41
+
42
+ The shared kernel imports no bounded context at all. Every context may import the shared kernel.
43
+
44
+ Every form of import counts, see [Every import counts](../index.md#every-import-counts).
45
+
46
+ ## What it reports
47
+
48
+ ```
49
+ src/ordering/driven/pg/adapters/stock.adapter.ts
50
+ 1 error strategic/no-cross-context-import: Imports
51
+ src/catalog/domain/aggregates/product.aggregate.ts (catalog domain):
52
+ only an OpenHostService of another bounded context may be imported.
53
+ 2 error strategic/no-cross-context-import: Imports the published language
54
+ of catalog: redeclare the fields you read in your own
55
+ published-language/.
56
+ 3 error strategic/no-cross-context-import: Uses the open host service of
57
+ catalog outside an AntiCorruptionLayer: translate it in an
58
+ anti-corruption layer.
59
+
60
+ src/catalog/catalog.module.ts
61
+ 2 error strategic/no-cross-context-import: The composition root
62
+ re-exports Product: it exports its own module only, so that no
63
+ other context reaches through it.
64
+
65
+ src/shared-kernel/domain/value-objects/money.value-object.ts
66
+ 1 error strategic/no-cross-context-import: The shared kernel imports no
67
+ bounded context, but imports
68
+ src/catalog/domain/value-objects/currency.value-object.ts
69
+ (catalog domain).
70
+ ```
71
+
72
+ ## Fix it
73
+
74
+ ### Go through an anti-corruption layer
75
+
76
+ So that only one class knows the other context exists, declare in your domain a port that asks in
77
+ your own words, and implement it in an anti-corruption layer that calls the open host service.
78
+
79
+ <div class="al-compare">
80
+
81
+ ```ts [❌ Avoid: src/ordering/driven/pg/adapters/stock.adapter.ts]
82
+ import type { Product } from "../../../../catalog/domain/aggregates/product.aggregate";
83
+ import type { ProductRepresentation } from "../../../../catalog/published-language/product.representation";
84
+ ```
85
+
86
+ ```ts [✅ Prefer: src/ordering/driven/catalog/adapters/catalog-price-list.adapter.ts]
87
+ import type { AntiCorruptionLayer } from "@alveolus/core";
88
+
89
+ import type { CatalogApi } from "../../../../catalog/driving/in-process/catalog-api";
90
+ import { PriceList } from "../../../domain/ports/price-list.port";
91
+
92
+ export class CatalogPriceList
93
+ extends PriceList
94
+ implements AntiCorruptionLayer
95
+ {
96
+ constructor(private readonly catalog: CatalogApi) {
97
+ super();
98
+ }
99
+ }
100
+ ```
101
+
102
+ </div>
103
+
104
+ ### Redeclare the fields you read
105
+
106
+ So that the upstream context can change its published language without breaking yours, the
107
+ downstream context redeclares the fields it reads in its own `published-language/`, instead of
108
+ importing the upstream types. When the open host service is called in the same process,
109
+ TypeScript still checks that both shapes match, at the anti-corruption layer and nowhere else.
110
+
111
+ ### Keep the shared kernel independent
112
+
113
+ So that a change in one context never reaches all the others, the shared kernel imports no
114
+ bounded context. Move what it needs into the shared kernel, or keep it in the context that owns it.
115
+
116
+ ## Limits
117
+
118
+ ::: warning What the rule cannot see
119
+ - The rule checks what an anti-corruption layer imports, not what it does with it: an adapter that
120
+ returns the open host service's answer as is, untranslated, is accepted. In review, the ACL
121
+ should build values of its own context.
122
+ - A file that matches `ignore` in `alveolus.config.ts` is not analysed at all.
123
+ :::
124
+
125
+ ## Turn it off
126
+
127
+ ```ts [alveolus.config.ts]
128
+ rules: { "strategic/no-cross-context-import": "off" },
129
+ ```
130
+
131
+ On an existing project, prefer a [baseline](../../guide/getting-started.md#adopt-it-on-an-existing-project):
132
+ new code keeps the boundaries while you remove the old shortcuts.
133
+
134
+ ## See also
135
+
136
+ - [Project layout: bounded contexts](../../guide/project-layout.md#bounded-contexts)
137
+ - [Open host services](../../core/strategic/open-host-services.md) and
138
+ [Anti-corruption layers](../../core/strategic/anti-corruption-layers.md)
139
+ - [`layers/no-outward-import`](../layers/no-outward-import.md), for dependencies inside a context
140
+ - [Rules](../index.md), every rule by category
@@ -0,0 +1,81 @@
1
+ ---
2
+ description: "Architecture rule: the shared kernel holds value objects, ports and their adapters, never an aggregate, a repository or a handler."
3
+ ---
4
+
5
+ # no-fat-shared-kernel
6
+
7
+ The shared kernel stays small: value objects, identifiers, errors, ports and their adapters.
8
+ Everything that changes with a business belongs to one bounded context.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Rule</dt><dd><code>strategic/no-fat-shared-kernel</code></dd>
12
+ <dt>Category</dt><dd><a href="/rules/#strategic">Strategic</a>: what crosses a bounded context</dd>
13
+ <dt>Reports</dt><dd>An <code>AggregateRoot</code>, an <code>Entity</code>, a <code>DomainEvent</code>, a <code>DomainService</code>, a repository, a handler or an <code>EventTranslator</code> in the shared kernel</dd>
14
+ <dt>Applies to</dt><dd>Every class of the shared kernel</dd>
15
+ <dt>Turn off</dt><dd><a href="#turn-it-off"><code>"strategic/no-fat-shared-kernel": "off"</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ `Customer` is needed by every context, so it lands in the shared kernel. From then on every
21
+ context depends on its shape, its rules and its repository; the KYC team cannot change how a
22
+ customer is verified without a change that reaches the whole system. The shared kernel has become
23
+ the one model nobody can touch.
24
+
25
+ ::: tip The fix
26
+ Each context keeps its own view of a customer, under its own name: a `Payer` in payments, an
27
+ `Applicant` in KYC, each with the fields it needs. What they share is small and stable: the
28
+ `CustomerId`, the `Money` value object, the ports every context uses.
29
+ :::
30
+
31
+ ## What it checks
32
+
33
+ Every class in the shared kernel, by what it extends:
34
+
35
+ | Class | Allowed |
36
+ | --- | --- |
37
+ | A `ValueObject`, an `Identifier`, a `DomainError` | ✅ |
38
+ | A `Port`, abstract or implemented by an adapter: a tracer, an outbox, a clock | ✅ |
39
+ | A representation of the published language | ✅ |
40
+ | An `AggregateRoot`, an `Entity`, a `DomainEvent`, a `DomainService` | ❌ |
41
+ | A `CommandRepository`, a `QueryRepository` | ❌ |
42
+ | A `CommandHandler`, a `QueryHandler`, an `EventTranslator` | ❌ |
43
+
44
+ ## What it reports
45
+
46
+ ```
47
+ src/shared-kernel/domain/aggregates/customer.aggregate.ts
48
+ 3 error strategic/no-fat-shared-kernel: Customer is an AggregateRoot in
49
+ the shared kernel: it belongs to one bounded context; the shared
50
+ kernel holds value objects, ports and their adapters.
51
+ ```
52
+
53
+ ## Fix it
54
+
55
+ ### Give the aggregate a home
56
+
57
+ So that one team owns it, move the aggregate, its repository and its handlers to the context that
58
+ decides its rules, and expose what the others need through an
59
+ [open host service](../../core/strategic/open-host-services.md). The identifier stays in the
60
+ shared kernel.
61
+
62
+ ## Limits
63
+
64
+ ::: warning What the rule cannot see
65
+ - How big the shared kernel is: three hundred value objects pass. In review, a value object enters
66
+ the shared kernel when two contexts already have the same one, not before.
67
+ - A folder declared as a bounded context named `shared` or `common` is a context, not the shared
68
+ kernel: the rule does not apply to it, and every other rule treats it as one more context.
69
+ :::
70
+
71
+ ## Turn it off
72
+
73
+ ```ts [alveolus.config.ts]
74
+ rules: { "strategic/no-fat-shared-kernel": "off" },
75
+ ```
76
+
77
+ ## See also
78
+
79
+ - [Project layout: shared kernel](../../guide/project-layout.md#shared-kernel)
80
+ - [`strategic/no-unmapped-context`](./no-unmapped-context.md), the other way contexts get tied
81
+ - [Rules](../index.md), every rule by category
@@ -0,0 +1,107 @@
1
+ ---
2
+ description: "Architecture rule: an open host service speaks the published language, and never exposes a class of its bounded context."
3
+ ---
4
+
5
+ # no-leaky-host-service
6
+
7
+ An open host service speaks the published language: no class of its context appears in what it
8
+ offers.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Rule</dt><dd><code>strategic/no-leaky-host-service</code></dd>
12
+ <dt>Category</dt><dd><a href="/rules/#strategic">Strategic</a>: what crosses a bounded context</dd>
13
+ <dt>Reports</dt><dd>A class of the project in the parameters, results, properties or getters of an open host service</dd>
14
+ <dt>Applies to</dt><dd>Every class that implements <code>OpenHostService</code></dd>
15
+ <dt>Turn off</dt><dd><a href="#turn-it-off"><code>"strategic/no-leaky-host-service": "off"</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ `CatalogApi.product` returns the `Product` aggregate, because it was already loaded. Ordering only
21
+ imports the open host service, as the rules ask, yet it now holds the catalog's model: a change to
22
+ `Product` breaks ordering, and ordering can call `product.changePrice()` from outside its
23
+ boundary.
24
+
25
+ ::: tip The fix
26
+ The service answers in the published language, plain JSON types that the catalog commits to keep
27
+ stable. The model behind it can change freely.
28
+ :::
29
+
30
+ ## What it checks
31
+
32
+ Every public method, property and getter of a class that implements `OpenHostService`: its
33
+ parameters and its result, followed into generics, `Promise`, arrays and object types.
34
+
35
+ | Type | Allowed |
36
+ | --- | --- |
37
+ | A published-language type, a plain value | ✅ |
38
+ | A class of the shared kernel, such as `Money` | ✅ |
39
+ | A class of an installed package | ✅ |
40
+ | Any other class of the project: an aggregate, an entity, a value object, an identifier, a domain event | ❌ |
41
+
42
+ The constructor and private members are left out: they wire the service, other contexts never see
43
+ them.
44
+
45
+ ## What it reports
46
+
47
+ ```
48
+ src/catalog/driving/in-process/catalog-api.ts
49
+ 6 error strategic/no-leaky-host-service: CatalogApi.product exposes
50
+ Product, an AggregateRoot of catalog: an open host service speaks
51
+ the published language.
52
+ ```
53
+
54
+ ## Fix it
55
+
56
+ ### Answer in the published language
57
+
58
+ So that the model can change without breaking other contexts, map it to a representation before it
59
+ leaves the service, and take plain values as parameters.
60
+
61
+ <div class="al-compare">
62
+
63
+ ```ts [❌ Avoid: src/catalog/driving/in-process/catalog-api.ts]
64
+ export class CatalogApi implements OpenHostService {
65
+ async product(id: ProductId): Promise<Product | undefined> {
66
+ return this.products.findById(id);
67
+ }
68
+ }
69
+ ```
70
+
71
+ ```ts [✅ Prefer: src/catalog/driving/in-process/catalog-api.ts]
72
+ export class CatalogApi implements OpenHostService {
73
+ async productById(productId: string): Promise<ProductRepresentation | undefined> {
74
+ const result = await this.getProduct.handle({ productId });
75
+ return result.ok ? result.value : undefined;
76
+ }
77
+ }
78
+ ```
79
+
80
+ </div>
81
+
82
+ ## Limits
83
+
84
+ ::: warning What the rule cannot see
85
+ - What a method returns as `unknown`, `any` or a plain object typed by hand with the fields of the
86
+ aggregate: the rule follows classes. In review, a method of an open host service returns a type of
87
+ `published-language/`.
88
+ - A DTO class of the context is a leak too, on purpose: the published language is made of types,
89
+ not classes.
90
+ :::
91
+
92
+ ## Turn it off
93
+
94
+ ```ts [alveolus.config.ts]
95
+ rules: { "strategic/no-leaky-host-service": "off" },
96
+ ```
97
+
98
+ On an existing project, prefer a [baseline](../../guide/getting-started.md#adopt-it-on-an-existing-project):
99
+ new methods answer in the published language while you map the old ones.
100
+
101
+ ## See also
102
+
103
+ - [Open host services](../../core/strategic/open-host-services.md), what is checked
104
+ - [Published Language](../../core/strategic/published-language.md), what the service answers in
105
+ - [`strategic/no-cross-context-import`](./no-cross-context-import.md), which makes the service the
106
+ only door of a context
107
+ - [Rules](../index.md), every rule by category