@alveolus/arch 0.2.0 → 0.3.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.
- package/README.md +6 -0
- package/dist/bin.mjs +4 -2
- package/dist/bin.mjs.map +1 -1
- package/dist/{cli-P5PwH9OE.mjs → docs-DsQHpTtV.mjs} +190 -5
- package/dist/docs-DsQHpTtV.mjs.map +1 -0
- package/dist/index.d.mts +44 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/docs/core/application/command-handlers.md +617 -0
- package/docs/core/application/event-publishers.md +234 -0
- package/docs/core/application/event-translators.md +329 -0
- package/docs/core/application/index.md +99 -0
- package/docs/core/application/integration-events.md +277 -0
- package/docs/core/application/outbox.md +416 -0
- package/docs/core/application/query-handlers.md +292 -0
- package/docs/core/application/unit-of-work.md +352 -0
- package/docs/core/domain/aggregates.md +822 -0
- package/docs/core/domain/domain-errors.md +251 -0
- package/docs/core/domain/domain-events.md +292 -0
- package/docs/core/domain/domain-services.md +249 -0
- package/docs/core/domain/entities.md +431 -0
- package/docs/core/domain/index.md +93 -0
- package/docs/core/domain/ports.md +284 -0
- package/docs/core/domain/repositories.md +335 -0
- package/docs/core/domain/value-objects.md +425 -0
- package/docs/core/domain/views.md +265 -0
- package/docs/core/index.md +108 -0
- package/docs/core/strategic/anti-corruption-layers.md +349 -0
- package/docs/core/strategic/index.md +83 -0
- package/docs/core/strategic/open-host-services.md +287 -0
- package/docs/core/strategic/published-language.md +265 -0
- package/docs/core/utilities/result.md +413 -0
- package/docs/guide/agents.md +68 -0
- package/docs/guide/existing-project.md +105 -0
- package/docs/guide/getting-started.md +275 -0
- package/docs/guide/learning-path.md +123 -0
- package/docs/guide/project-layout.md +324 -0
- package/docs/guide/versioning.md +42 -0
- package/docs/integrations/index.md +112 -0
- package/docs/integrations/nestjs.md +169 -0
- package/docs/rules/index.md +183 -0
- package/docs/rules/layers/no-driving-shortcut.md +119 -0
- package/docs/rules/layers/no-impure-domain.md +189 -0
- package/docs/rules/layers/no-outward-import.md +184 -0
- package/docs/rules/layers/no-portless-adapter.md +123 -0
- package/docs/rules/strategic/no-cross-context-import.md +140 -0
- package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
- package/docs/rules/strategic/no-leaky-host-service.md +107 -0
- package/docs/rules/strategic/no-unmapped-context.md +111 -0
- package/docs/rules/tactical/no-aggregate-reference.md +139 -0
- package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
- package/docs/rules/tactical/no-foreign-query-dependency.md +106 -0
- package/docs/rules/tactical/no-loose-code.md +171 -0
- package/docs/rules/tactical/no-misplaced-class.md +146 -0
- package/docs/rules/tactical/no-public-field.md +113 -0
- package/docs/rules/tactical/no-stateful-service.md +102 -0
- package/docs/rules/tactical/no-thrown-failure.md +162 -0
- package/docs/rules/tooling/no-loose-disable.md +98 -0
- package/package.json +4 -3
- package/dist/cli-P5PwH9OE.mjs.map +0 -1
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Anti-corruption layers in Domain-Driven Design with TypeScript: translate another bounded context into your own language so its model never leaks in."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Anti-corruption layers
|
|
6
|
+
|
|
7
|
+
An anti-corruption layer is the adapter that reads another bounded context and translates it into
|
|
8
|
+
the language of yours, so that the foreign model never enters your domain or your application.
|
|
9
|
+
|
|
10
|
+
<dl class="al-glance">
|
|
11
|
+
<dt>Layer</dt><dd>Driven, or driving for a consumer</dd>
|
|
12
|
+
<dt>File</dt><dd><code>src/ordering/driven/catalog/adapters/catalog-price-list.adapter.ts</code></dd>
|
|
13
|
+
<dt>Implements</dt><dd><a href="#api"><code>AntiCorruptionLayer</code></a>, and extends your <a href="/core/domain/ports">port</a></dd>
|
|
14
|
+
<dt>Called by</dt><dd><a href="/core/application/command-handlers">Command handlers</a>, through the port</dd>
|
|
15
|
+
<dt>Checked by</dt><dd><a href="/rules/strategic/no-cross-context-import"><code>strategic/no-cross-context-import</code></a>, <a href="/rules/layers/no-portless-adapter"><code>layers/no-portless-adapter</code></a>, <a href="/rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a></dd>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
Before adding a line to an order, ordering checks that the product has a price in the catalog. If
|
|
21
|
+
the handler calls `CatalogApi` directly, the catalog's representations spread into the use case,
|
|
22
|
+
then into the domain. Every change in the catalog becomes a change in ordering, and the order
|
|
23
|
+
starts speaking the catalog's language.
|
|
24
|
+
|
|
25
|
+
::: tip The fix
|
|
26
|
+
One adapter, and only one, touches the catalog. Ordering asks for what it needs in its own words,
|
|
27
|
+
through a [port](../domain/ports.md); the anti-corruption layer calls the catalog, reads its JSON
|
|
28
|
+
and answers with ordering's objects.
|
|
29
|
+
:::
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
<div class="al-cards">
|
|
34
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>The domain states its need</span>The <code>PriceList</code> port asks for the price of a <code>ProductId</code>, as <code>Money</code>. It never mentions the catalog.</div>
|
|
35
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>The adapter calls the other context</span><code>CatalogPriceList</code> calls the <a href="/core/strategic/open-host-services">open host service</a> and reads its <a href="/core/strategic/published-language">published language</a>.</div>
|
|
36
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>It answers with your objects</span>It turns the JSON into <code>Money</code>. Representations come in; they never go out.</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
## Where it fits
|
|
40
|
+
|
|
41
|
+
The anti-corruption layer is the ordering side of the meeting point. The handler only sees the
|
|
42
|
+
port; the adapter is the one place that knows the catalog exists.
|
|
43
|
+
|
|
44
|
+
<div class="al-diagram">
|
|
45
|
+
<svg viewBox="0 0 720 260" role="img" aria-label="In ordering, AddLineHandler asks the PriceList port for a price. CatalogPriceList, the anti-corruption layer, extends the port and reads the JSON answered by CatalogApi, the open host service of catalog, which calls GetProductHandler.">
|
|
46
|
+
<defs>
|
|
47
|
+
<marker id="acl-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
48
|
+
<path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
|
|
49
|
+
</marker>
|
|
50
|
+
</defs>
|
|
51
|
+
<rect class="box" x="8" y="8" width="272" height="244" rx="14" />
|
|
52
|
+
<text class="note" x="24" y="30">src/ordering/</text>
|
|
53
|
+
<rect class="box" x="24" y="44" width="240" height="48" rx="8" />
|
|
54
|
+
<text class="label" x="144" y="64" text-anchor="middle">AddLineHandler</text>
|
|
55
|
+
<text class="note" x="144" y="82" text-anchor="middle">asks for a price</text>
|
|
56
|
+
<rect class="box" x="24" y="116" width="240" height="48" rx="8" />
|
|
57
|
+
<text class="label" x="144" y="136" text-anchor="middle">PriceList</text>
|
|
58
|
+
<text class="note" x="144" y="154" text-anchor="middle">port · in your words</text>
|
|
59
|
+
<rect class="boundary" x="24" y="188" width="240" height="48" rx="8" />
|
|
60
|
+
<text class="label" x="144" y="208" text-anchor="middle">CatalogPriceList</text>
|
|
61
|
+
<text class="note" x="144" y="226" text-anchor="middle">anti-corruption layer</text>
|
|
62
|
+
<path class="link" d="M 144 92 L 144 114" marker-end="url(#acl-flow-arrow)" />
|
|
63
|
+
<path class="link" d="M 144 188 L 144 166" marker-end="url(#acl-flow-arrow)" />
|
|
64
|
+
<text class="note" x="154" y="181">extends</text>
|
|
65
|
+
<rect class="box" x="300" y="188" width="156" height="48" rx="8" />
|
|
66
|
+
<text class="label" x="378" y="208" text-anchor="middle">JSON</text>
|
|
67
|
+
<text class="note" x="378" y="226" text-anchor="middle">the contract</text>
|
|
68
|
+
<path class="link" d="M 300 212 L 266 212" marker-end="url(#acl-flow-arrow)" />
|
|
69
|
+
<path class="link" d="M 492 212 L 458 212" marker-end="url(#acl-flow-arrow)" />
|
|
70
|
+
<rect class="box" x="476" y="8" width="236" height="244" rx="14" />
|
|
71
|
+
<text class="note" x="492" y="30">src/catalog/</text>
|
|
72
|
+
<rect class="box" x="492" y="44" width="204" height="48" rx="8" />
|
|
73
|
+
<text class="label" x="594" y="64" text-anchor="middle">GetProductHandler</text>
|
|
74
|
+
<text class="note" x="594" y="82" text-anchor="middle">reads the catalog</text>
|
|
75
|
+
<rect class="box" x="492" y="188" width="204" height="48" rx="8" />
|
|
76
|
+
<text class="label" x="594" y="208" text-anchor="middle">CatalogApi</text>
|
|
77
|
+
<text class="note" x="594" y="226" text-anchor="middle">open host service</text>
|
|
78
|
+
<path class="link" d="M 594 188 L 594 94" marker-end="url(#acl-flow-arrow)" />
|
|
79
|
+
<text class="note" x="604" y="145">calls</text>
|
|
80
|
+
</svg>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
::: tip
|
|
84
|
+
If the catalog moves behind HTTP, only the anti-corruption layer changes: it fetches and validates
|
|
85
|
+
the JSON instead of calling `CatalogApi`. The port, the handler and the domain stay the same.
|
|
86
|
+
:::
|
|
87
|
+
|
|
88
|
+
## API
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import type { AntiCorruptionLayer } from "@alveolus/core";
|
|
92
|
+
// or: import type { AntiCorruptionLayer } from
|
|
93
|
+
// "@alveolus/core/anti-corruption-layers";
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `AntiCorruptionLayer` <Badge type="info" text="abstract · no members" /> <Badge type="tip" text="you implement it" />
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
abstract class AntiCorruptionLayer {}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Marks the class as the translator of another context. It has no members: the class declares its
|
|
103
|
+
role with `implements`, and the rules recognise it.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
export class CatalogPriceList
|
|
107
|
+
extends PriceList
|
|
108
|
+
implements AntiCorruptionLayer { … }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### The methods of the port <Badge type="tip" text="you implement them" />
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
async priceOf(productId: ProductId): Promise<Money | undefined>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For a driven anti-corruption layer: the methods of the [port](../domain/ports.md) it extends,
|
|
118
|
+
such as `priceOf` of `PriceList`. Each one calls the other context, reads its representation and
|
|
119
|
+
returns your objects. Your handlers call them through the port, in your language.
|
|
120
|
+
|
|
121
|
+
### `consume(event)` <Badge type="info" text="any name" /> <Badge type="tip" text="called by your message broker" />
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
async consume(event: OrderPlacedRepresentation): Promise<void>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
For a driving anti-corruption layer: receives an event of the other context, in its published
|
|
128
|
+
language, and turns it into one of your commands.
|
|
129
|
+
|
|
130
|
+
::: warning Caveats
|
|
131
|
+
- Use `implements`, not `extends`: a driven anti-corruption layer already extends its port.
|
|
132
|
+
- It is the only place, with the composition root, that may import another bounded context: see
|
|
133
|
+
[`strategic/no-cross-context-import`](../../rules/strategic/no-cross-context-import.md).
|
|
134
|
+
- A driven anti-corruption layer is placed like any adapter, in `driven/<context>/adapters/`; a
|
|
135
|
+
consumer goes in `driving/`: see [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md).
|
|
136
|
+
:::
|
|
137
|
+
|
|
138
|
+
## Usage
|
|
139
|
+
|
|
140
|
+
Build the anti-corruption layer through which ordering reads prices from the catalog, one idea at a
|
|
141
|
+
time. Each step shows the whole file: added lines are highlighted, replaced lines are struck out.
|
|
142
|
+
|
|
143
|
+
<div class="al-cards">
|
|
144
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-declare-what-your-context-needs">Declare what your context needs</a></span>Name the need in your own words.</div>
|
|
145
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-the-adapter">Declare the adapter</a></span>One class at the boundary.</div>
|
|
146
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-ask-the-other-context">Ask the other context</a></span>Call its open host service.</div>
|
|
147
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-type-the-answer-in-your-own-words">Type the answer in your own words</a></span>Redeclare what you read.</div>
|
|
148
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-translate-into-your-model">Translate into your model</a></span>Answer with your objects.</div>
|
|
149
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">6</span><a href="#_6-wire-it">Wire it</a></span>Hand it the open host service.</div>
|
|
150
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">7</span><a href="#_7-check-it">Check it</a></span>Let the rules keep it that way.</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
### 1. Declare what your context needs
|
|
154
|
+
|
|
155
|
+
The domain of ordering asks for prices through its own [port](../domain/ports.md), `PriceList`,
|
|
156
|
+
with an abstract `priceOf(productId)` that returns a `Money` or `undefined`.
|
|
157
|
+
|
|
158
|
+
### 2. Declare the adapter
|
|
159
|
+
|
|
160
|
+
So that the rest of ordering never learns the catalog exists, one driven adapter extends the port
|
|
161
|
+
and implements `AntiCorruptionLayer`. It receives `CatalogApi`, the open host service of the
|
|
162
|
+
catalog, in its constructor.
|
|
163
|
+
|
|
164
|
+
```ts [src/ordering/driven/catalog/adapters/catalog-price-list.adapter.ts]
|
|
165
|
+
import type { AntiCorruptionLayer } from "@alveolus/core";
|
|
166
|
+
|
|
167
|
+
import { CatalogApi } from
|
|
168
|
+
"../../../../catalog/driving/in-process/catalog-api";
|
|
169
|
+
import { PriceList } from "../../../domain/ports/price-list.port";
|
|
170
|
+
|
|
171
|
+
export class CatalogPriceList
|
|
172
|
+
extends PriceList
|
|
173
|
+
implements AntiCorruptionLayer
|
|
174
|
+
{
|
|
175
|
+
constructor(private readonly catalog: CatalogApi) {
|
|
176
|
+
super();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
TypeScript now asks for `priceOf`: the next step adds it.
|
|
182
|
+
|
|
183
|
+
### 3. Ask the other context
|
|
184
|
+
|
|
185
|
+
The adapter calls the catalog in the catalog's terms: a raw string id, and `undefined` when the
|
|
186
|
+
product does not exist. Ordering's `ProductId` stays on this side.
|
|
187
|
+
|
|
188
|
+
```ts [src/ordering/driven/catalog/adapters/catalog-price-list.adapter.ts]
|
|
189
|
+
import type { AntiCorruptionLayer } from "@alveolus/core";
|
|
190
|
+
|
|
191
|
+
import { CatalogApi } from
|
|
192
|
+
"../../../../catalog/driving/in-process/catalog-api";
|
|
193
|
+
import { Money } from // [!code ++]
|
|
194
|
+
"../../../../shared-kernel/domain/value-objects/money.value-object"; // [!code ++]
|
|
195
|
+
import { PriceList } from "../../../domain/ports/price-list.port";
|
|
196
|
+
import type { ProductId } from // [!code ++]
|
|
197
|
+
"../../../domain/value-objects/product-id.identifier"; // [!code ++]
|
|
198
|
+
|
|
199
|
+
export class CatalogPriceList
|
|
200
|
+
extends PriceList
|
|
201
|
+
implements AntiCorruptionLayer
|
|
202
|
+
{
|
|
203
|
+
constructor(private readonly catalog: CatalogApi) {
|
|
204
|
+
super();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async priceOf(productId: ProductId): Promise<Money | undefined> { // [!code ++]
|
|
208
|
+
const product = // [!code ++]
|
|
209
|
+
await this.catalog.productById(productId.value); // [!code ++]
|
|
210
|
+
if (product === undefined) { // [!code ++]
|
|
211
|
+
return undefined; // [!code ++]
|
|
212
|
+
} // [!code ++]
|
|
213
|
+
} // [!code ++]
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 4. Type the answer in your own words
|
|
218
|
+
|
|
219
|
+
So that a change in the catalog breaks the build instead of production, the answer is typed with
|
|
220
|
+
ordering's own [published language](./published-language.md): `CatalogProductRepresentation`
|
|
221
|
+
redeclares the fields ordering reads.
|
|
222
|
+
|
|
223
|
+
```ts [src/ordering/driven/catalog/adapters/catalog-price-list.adapter.ts]
|
|
224
|
+
import type { AntiCorruptionLayer } from "@alveolus/core";
|
|
225
|
+
|
|
226
|
+
import { CatalogApi } from
|
|
227
|
+
"../../../../catalog/driving/in-process/catalog-api";
|
|
228
|
+
import { Money } from
|
|
229
|
+
"../../../../shared-kernel/domain/value-objects/money.value-object";
|
|
230
|
+
import { PriceList } from "../../../domain/ports/price-list.port";
|
|
231
|
+
import type { ProductId } from
|
|
232
|
+
"../../../domain/value-objects/product-id.identifier";
|
|
233
|
+
import type { CatalogProductRepresentation } from // [!code ++]
|
|
234
|
+
"../../../published-language/catalog-product.representation"; // [!code ++]
|
|
235
|
+
|
|
236
|
+
export class CatalogPriceList
|
|
237
|
+
extends PriceList
|
|
238
|
+
implements AntiCorruptionLayer
|
|
239
|
+
{
|
|
240
|
+
constructor(private readonly catalog: CatalogApi) {
|
|
241
|
+
super();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async priceOf(productId: ProductId): Promise<Money | undefined> {
|
|
245
|
+
const product = // [!code --]
|
|
246
|
+
const product: CatalogProductRepresentation | undefined = // [!code ++]
|
|
247
|
+
await this.catalog.productById(productId.value);
|
|
248
|
+
if (product === undefined) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 5. Translate into your model
|
|
256
|
+
|
|
257
|
+
The domain expects a `Money`, not JSON: the adapter builds the `Currency`, then the `Money`, with
|
|
258
|
+
the factories of the value objects, chained by `andThen`. An invalid price is a broken contract,
|
|
259
|
+
not a business failure, so the adapter throws.
|
|
260
|
+
|
|
261
|
+
```ts [src/ordering/driven/catalog/adapters/catalog-price-list.adapter.ts]
|
|
262
|
+
import type { AntiCorruptionLayer } from "@alveolus/core"; // [!code --]
|
|
263
|
+
import { type AntiCorruptionLayer, andThen } from "@alveolus/core"; // [!code ++]
|
|
264
|
+
|
|
265
|
+
import { CatalogApi } from
|
|
266
|
+
"../../../../catalog/driving/in-process/catalog-api";
|
|
267
|
+
import { Currency } from // [!code ++]
|
|
268
|
+
"../../../../shared-kernel/domain/value-objects/currency.value-object";// [!code ++]
|
|
269
|
+
import { Money } from
|
|
270
|
+
"../../../../shared-kernel/domain/value-objects/money.value-object";
|
|
271
|
+
import { PriceList } from "../../../domain/ports/price-list.port";
|
|
272
|
+
import type { ProductId } from
|
|
273
|
+
"../../../domain/value-objects/product-id.identifier";
|
|
274
|
+
import type { CatalogProductRepresentation } from
|
|
275
|
+
"../../../published-language/catalog-product.representation";
|
|
276
|
+
|
|
277
|
+
export class CatalogPriceList
|
|
278
|
+
extends PriceList
|
|
279
|
+
implements AntiCorruptionLayer
|
|
280
|
+
{
|
|
281
|
+
constructor(private readonly catalog: CatalogApi) {
|
|
282
|
+
super();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async priceOf(productId: ProductId): Promise<Money | undefined> {
|
|
286
|
+
const product: CatalogProductRepresentation | undefined =
|
|
287
|
+
await this.catalog.productById(productId.value);
|
|
288
|
+
if (product === undefined) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
const { amount, currency } = product.price; // [!code ++]
|
|
292
|
+
const price = andThen(Currency.of(currency), (code) => // [!code ++]
|
|
293
|
+
Money.of(amount, code), // [!code ++]
|
|
294
|
+
); // [!code ++]
|
|
295
|
+
if (!price.ok) { // [!code ++]
|
|
296
|
+
const reason = price.error.type; // [!code ++]
|
|
297
|
+
throw new Error( // [!code ++]
|
|
298
|
+
`The catalog sent an invalid price: ${reason}`, // [!code ++]
|
|
299
|
+
); // [!code ++]
|
|
300
|
+
} // [!code ++]
|
|
301
|
+
return price.value; // [!code ++]
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 6. Wire it
|
|
307
|
+
|
|
308
|
+
The composition root of ordering receives the catalog's module and builds the adapter with its open
|
|
309
|
+
host service. The handlers only see `PriceList`.
|
|
310
|
+
|
|
311
|
+
```ts [src/ordering/ordering.module.ts]
|
|
312
|
+
export class OrderingModule {
|
|
313
|
+
constructor(db: Pool, catalog: CatalogModule) {
|
|
314
|
+
const prices = new CatalogPriceList(catalog.api);
|
|
315
|
+
…
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### 7. Check it
|
|
321
|
+
|
|
322
|
+
Run the checks. Three rules keep the boundary where it is now:
|
|
323
|
+
|
|
324
|
+
```sh
|
|
325
|
+
npx alveolus arch check
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
<div class="al-cards">
|
|
329
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/strategic/no-cross-context-import"><code>strategic/no-cross-context-import</code></a></span>Only the anti-corruption layer may use the open host service of another context.</div>
|
|
330
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-portless-adapter"><code>layers/no-portless-adapter</code></a></span>The adapter extends a port declared by the domain.</div>
|
|
331
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a></span>It stays in <code>driven/<technology>/adapters/*.adapter.ts</code>.</div>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
A handler that calls the catalog directly is reported:
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
src/ordering/application/commands/place-order.command.ts
|
|
338
|
+
3 error strategic/no-cross-context-import: Uses the open host service of
|
|
339
|
+
catalog outside an AntiCorruptionLayer: translate it in an
|
|
340
|
+
anti-corruption layer.
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## See also
|
|
344
|
+
|
|
345
|
+
- [Open host services](./open-host-services.md), what a driven anti-corruption layer calls
|
|
346
|
+
- [Published Language](./published-language.md), what it reads
|
|
347
|
+
- [Ports](../domain/ports.md), what a driven anti-corruption layer extends
|
|
348
|
+
- [Project layout: bounded contexts](../../guide/project-layout.md#bounded-contexts)
|
|
349
|
+
- Rules: [`strategic/no-cross-context-import`](../../rules/strategic/no-cross-context-import.md), [`layers/no-portless-adapter`](../../rules/layers/no-portless-adapter.md), [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Strategic Domain-Driven Design in TypeScript: split a system into bounded contexts and decide how they talk without sharing their models."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Strategic
|
|
6
|
+
|
|
7
|
+
Strategic design splits a system into bounded contexts, each with its own model, and decides how
|
|
8
|
+
they talk without sharing that model.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
The catalog knows a product by its name, photos and stock. Ordering only needs its price. If
|
|
13
|
+
ordering imports the catalog's `Product` class, every change in the catalog breaks ordering, and
|
|
14
|
+
the two teams can no longer move alone.
|
|
15
|
+
|
|
16
|
+
::: tip The fix
|
|
17
|
+
Each context keeps its model to itself. They exchange plain JSON through a documented entry point,
|
|
18
|
+
and each side translates it into its own words.
|
|
19
|
+
:::
|
|
20
|
+
|
|
21
|
+
## How the contexts meet
|
|
22
|
+
|
|
23
|
+
<div class="al-diagram">
|
|
24
|
+
<svg viewBox="0 0 720 260" role="img" aria-label="In ordering, AddLineHandler asks the PriceList port for a price. CatalogPriceList, the anti-corruption layer, extends the port and reads the JSON of the published language, answered by CatalogApi, the open host service of catalog, which calls GetProductHandler.">
|
|
25
|
+
<defs>
|
|
26
|
+
<marker id="strategic-overview-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
27
|
+
<path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
|
|
28
|
+
</marker>
|
|
29
|
+
</defs>
|
|
30
|
+
<rect class="boundary" x="8" y="8" width="272" height="244" rx="14" />
|
|
31
|
+
<text class="note" x="24" y="30">src/ordering/ · a context</text>
|
|
32
|
+
<rect class="box" x="24" y="44" width="240" height="48" rx="8" />
|
|
33
|
+
<text class="label" x="144" y="64" text-anchor="middle">AddLineHandler</text>
|
|
34
|
+
<text class="note" x="144" y="82" text-anchor="middle">asks for a price</text>
|
|
35
|
+
<rect class="box" x="24" y="116" width="240" height="48" rx="8" />
|
|
36
|
+
<text class="label" x="144" y="136" text-anchor="middle">PriceList</text>
|
|
37
|
+
<text class="note" x="144" y="154" text-anchor="middle">port · in your words</text>
|
|
38
|
+
<rect class="box" x="24" y="188" width="240" height="48" rx="8" />
|
|
39
|
+
<text class="label" x="144" y="208" text-anchor="middle">CatalogPriceList</text>
|
|
40
|
+
<text class="note" x="144" y="226" text-anchor="middle">anti-corruption layer</text>
|
|
41
|
+
<path class="link" d="M 144 92 L 144 114" marker-end="url(#strategic-overview-arrow)" />
|
|
42
|
+
<path class="link" d="M 144 188 L 144 166" marker-end="url(#strategic-overview-arrow)" />
|
|
43
|
+
<text class="note" x="154" y="181">extends</text>
|
|
44
|
+
<rect class="box" x="300" y="188" width="156" height="48" rx="8" />
|
|
45
|
+
<text class="label" x="378" y="208" text-anchor="middle">JSON</text>
|
|
46
|
+
<text class="note" x="378" y="226" text-anchor="middle">published language</text>
|
|
47
|
+
<path class="link" d="M 300 212 L 266 212" marker-end="url(#strategic-overview-arrow)" />
|
|
48
|
+
<path class="link" d="M 492 212 L 458 212" marker-end="url(#strategic-overview-arrow)" />
|
|
49
|
+
<rect class="boundary" x="476" y="8" width="236" height="244" rx="14" />
|
|
50
|
+
<text class="note" x="492" y="30">src/catalog/ · a context</text>
|
|
51
|
+
<rect class="box" x="492" y="44" width="204" height="48" rx="8" />
|
|
52
|
+
<text class="label" x="594" y="64" text-anchor="middle">GetProductHandler</text>
|
|
53
|
+
<text class="note" x="594" y="82" text-anchor="middle">reads the catalog</text>
|
|
54
|
+
<rect class="box" x="492" y="188" width="204" height="48" rx="8" />
|
|
55
|
+
<text class="label" x="594" y="208" text-anchor="middle">CatalogApi</text>
|
|
56
|
+
<text class="note" x="594" y="226" text-anchor="middle">open host service</text>
|
|
57
|
+
<path class="link" d="M 594 188 L 594 94" marker-end="url(#strategic-overview-arrow)" />
|
|
58
|
+
<text class="note" x="604" y="145">calls</text>
|
|
59
|
+
</svg>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="al-cards">
|
|
63
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>The catalog opens a door</span>Its <a href="/core/strategic/open-host-services">open host service</a> is the only class other contexts may call.</div>
|
|
64
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>It answers in JSON</span>The <a href="/core/strategic/published-language">published language</a> is the contract: plain data, versioned, no class.</div>
|
|
65
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Ordering translates</span>Its <a href="/core/strategic/anti-corruption-layers">anti-corruption layer</a> turns that JSON into its own words, behind a port.</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
## The building blocks
|
|
69
|
+
|
|
70
|
+
| Building block | What it is | Use it when |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| [Published Language](./published-language.md) | The JSON format exchanged between contexts. | Data crosses a context boundary, as a question, an answer or an event. |
|
|
73
|
+
| [Open host services](./open-host-services.md) | The documented entry point of a context, the only class others may import. | Another context needs to ask yours something. |
|
|
74
|
+
| [Anti-corruption layers](./anti-corruption-layers.md) | The adapter that reads another context and translates it into yours. | Your context needs something from another one. |
|
|
75
|
+
|
|
76
|
+
The events a context sends to the others are [integration events](../application/integration-events.md),
|
|
77
|
+
written in the published language.
|
|
78
|
+
|
|
79
|
+
## See also
|
|
80
|
+
|
|
81
|
+
- [Bounded contexts](../../guide/project-layout.md#bounded-contexts) in the project layout
|
|
82
|
+
- Rule: [`strategic/no-cross-context-import`](../../rules/strategic/no-cross-context-import.md)
|
|
83
|
+
- [Domain](../domain/index.md), the model inside each context
|