@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.
- package/README.md +11 -0
- package/dist/bin.mjs +4 -2
- package/dist/bin.mjs.map +1 -1
- package/dist/{cli-P5PwH9OE.mjs → docs-DcFgskuN.mjs} +214 -43
- package/dist/docs-DcFgskuN.mjs.map +1 -0
- package/dist/index.d.mts +53 -12
- 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 +108 -0
- package/docs/guide/getting-started.md +286 -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 +185 -0
- package/docs/rules/layers/no-driving-shortcut.md +119 -0
- package/docs/rules/layers/no-impure-domain.md +191 -0
- package/docs/rules/layers/no-outward-import.md +186 -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 +114 -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 +201 -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,265 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Views in CQRS with TypeScript: the read-only shape a query returns, built for the screen or API that shows it, without loading aggregates."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Views
|
|
6
|
+
|
|
7
|
+
A view is the read-only shape a query returns, built for the screen or the API that shows it.
|
|
8
|
+
|
|
9
|
+
<dl class="al-glance">
|
|
10
|
+
<dt>Layer</dt><dd>Domain</dd>
|
|
11
|
+
<dt>File</dt><dd><code>domain/views/order-summary.view.ts</code></dd>
|
|
12
|
+
<dt>Type</dt><dd><a href="#api"><code>View<Props></code></a></dd>
|
|
13
|
+
<dt>Read by</dt><dd><a href="/core/domain/repositories">Query repositories</a></dd>
|
|
14
|
+
<dt>Returned by</dt><dd><a href="/core/application/query-handlers">Query handlers</a></dd>
|
|
15
|
+
<dt>Checked by</dt><dd><a href="/rules/tactical/no-foreign-command-dependency"><code>tactical/no-foreign-command-dependency</code></a></dd>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
The orders screen shows each order's status and number of lines. Loading fifty `Order` aggregates,
|
|
21
|
+
with all their lines, just to count them is slow. It also hands the screen an object with
|
|
22
|
+
`place()` and `addLine()`: a read could change an order.
|
|
23
|
+
|
|
24
|
+
::: tip The fix
|
|
25
|
+
A view is a plain read-only type shaped for that screen: `OrderSummary`. A query repository builds
|
|
26
|
+
it straight from storage, without loading the aggregate. It carries data, so reading can never
|
|
27
|
+
write.
|
|
28
|
+
:::
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
An aggregate and a view describe the same order for two different jobs.
|
|
33
|
+
|
|
34
|
+
| | Aggregate | View |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| **Shaped for** | the business rules | a screen or an API |
|
|
37
|
+
| **Has methods** | yes, the business methods | no, only fields |
|
|
38
|
+
| **Read through** | a command repository | a query repository |
|
|
39
|
+
| **Can change** | through its methods | never |
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export type OrderSummary = View<{
|
|
43
|
+
readonly id: OrderId;
|
|
44
|
+
readonly customerId: CustomerId;
|
|
45
|
+
readonly status: "draft" | "placed";
|
|
46
|
+
readonly lineCount: number;
|
|
47
|
+
}>;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Where it fits
|
|
51
|
+
|
|
52
|
+
A query never touches the aggregate. The [query handler](../application/query-handlers.md) asks a
|
|
53
|
+
[query repository](./repositories.md) for the view, and returns it.
|
|
54
|
+
|
|
55
|
+
<div class="al-diagram">
|
|
56
|
+
<svg viewBox="0 0 680 200" role="img" aria-label="A request goes from a controller to the GetOrderSummaryHandler, which asks the OrderSummaries query repository and returns an OrderSummary view.">
|
|
57
|
+
<defs>
|
|
58
|
+
<marker id="view-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
59
|
+
<path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
|
|
60
|
+
</marker>
|
|
61
|
+
</defs>
|
|
62
|
+
<rect class="box" x="8" y="72" width="130" height="56" rx="8" />
|
|
63
|
+
<text class="label" x="73" y="96" text-anchor="middle">Controller</text>
|
|
64
|
+
<text class="note" x="73" y="116" text-anchor="middle">driving adapter</text>
|
|
65
|
+
<path class="link" d="M 138 100 L 168 100" marker-end="url(#view-flow-arrow)" />
|
|
66
|
+
<rect class="box" x="170" y="72" width="210" height="56" rx="8" />
|
|
67
|
+
<text class="label" x="275" y="96" text-anchor="middle">GetOrderSummaryHandler</text>
|
|
68
|
+
<text class="note" x="275" y="116" text-anchor="middle">query handler</text>
|
|
69
|
+
<rect class="box" x="440" y="36" width="232" height="48" rx="8" />
|
|
70
|
+
<text class="label" x="556" y="56" text-anchor="middle">1 · summaries.summaryOf(id)</text>
|
|
71
|
+
<text class="note" x="556" y="74" text-anchor="middle">reads storage, no aggregate</text>
|
|
72
|
+
<rect class="boundary" x="440" y="116" width="232" height="48" rx="8" />
|
|
73
|
+
<text class="label" x="556" y="136" text-anchor="middle">2 · OrderSummary</text>
|
|
74
|
+
<text class="note" x="556" y="154" text-anchor="middle">this page: what it returns</text>
|
|
75
|
+
<path class="link" d="M 380 100 L 438 60" marker-end="url(#view-flow-arrow)" />
|
|
76
|
+
<path class="link" d="M 438 140 L 382 104" marker-end="url(#view-flow-arrow)" />
|
|
77
|
+
</svg>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
::: tip
|
|
81
|
+
The driving adapter turns the view into its response, or into a
|
|
82
|
+
[representation](../strategic/published-language.md) for another context.
|
|
83
|
+
:::
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import type { View } from "@alveolus/core";
|
|
89
|
+
// or: import type { View } from "@alveolus/core/views";
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Type parameters
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
type View<Props extends object> = Readonly<Props>;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Parameter | What it is | Constraint |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| `Props` | The fields of the view. | an object type |
|
|
101
|
+
|
|
102
|
+
### Declaration
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
type OrderSummary = View<{
|
|
106
|
+
readonly id: OrderId;
|
|
107
|
+
readonly lineCount: number;
|
|
108
|
+
}>;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The view, in `domain/views/`: the fields the reader needs, read-only. It has no members.
|
|
112
|
+
|
|
113
|
+
::: warning Caveats
|
|
114
|
+
- `View` changes nothing at runtime: it is `Readonly<Props>`. It marks the type as the answer of a
|
|
115
|
+
query, for you and for your reviewers.
|
|
116
|
+
- `Readonly` is shallow: nested arrays and objects stay mutable unless you declare them `readonly`.
|
|
117
|
+
- No separate read model is implied: views are read from the same storage as the aggregates. CQRS
|
|
118
|
+
with separate stores is out of scope.
|
|
119
|
+
:::
|
|
120
|
+
|
|
121
|
+
## Usage
|
|
122
|
+
|
|
123
|
+
Build `OrderSummary`, what a screen shows for one order, then the query repository and the adapter
|
|
124
|
+
that read it. Each step shows the whole file it changes: added lines are highlighted, replaced lines are struck out.
|
|
125
|
+
|
|
126
|
+
<div class="al-cards">
|
|
127
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-know-who-reads-it">Know who reads it</a></span>One screen, one answer.</div>
|
|
128
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-the-view">Declare the view</a></span>Read-only data, no behaviour.</div>
|
|
129
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-declare-where-it-comes-from">Declare where it comes from</a></span>A query repository.</div>
|
|
130
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-build-it-from-storage">Build it from storage</a></span>No aggregate loaded.</div>
|
|
131
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-return-it-from-a-query-handler">Return it from a query handler</a></span>The query side only.</div>
|
|
132
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">6</span><a href="#_6-check-it">Check it</a></span>Let the rules keep it that way.</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
### 1. Know who reads it
|
|
136
|
+
|
|
137
|
+
A view answers one screen or one API response: here, the summary of an order, read without
|
|
138
|
+
loading the [`Order` aggregate](./aggregates.md).
|
|
139
|
+
|
|
140
|
+
### 2. Declare the view
|
|
141
|
+
|
|
142
|
+
A view is plain, read-only data shaped for its reader: a `View` type, not a class, so it has no
|
|
143
|
+
behaviour to keep in sync with the model.
|
|
144
|
+
|
|
145
|
+
```ts [src/ordering/domain/views/order-summary.view.ts]
|
|
146
|
+
import type { View } from "@alveolus/core";
|
|
147
|
+
|
|
148
|
+
import type {
|
|
149
|
+
CustomerId,
|
|
150
|
+
} from "../value-objects/customer-id.identifier";
|
|
151
|
+
import type { OrderId } from "../value-objects/order-id.identifier";
|
|
152
|
+
|
|
153
|
+
export type OrderSummary = View<{
|
|
154
|
+
readonly id: OrderId;
|
|
155
|
+
readonly customerId: CustomerId;
|
|
156
|
+
readonly status: "draft" | "placed";
|
|
157
|
+
readonly lineCount: number;
|
|
158
|
+
}>;
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 3. Declare where it comes from
|
|
162
|
+
|
|
163
|
+
The query side gets its own [repository](./repositories.md#queryrepository), declared in the
|
|
164
|
+
domain, that returns views only.
|
|
165
|
+
|
|
166
|
+
```ts [src/ordering/domain/repositories/order-summaries.repository.ts]
|
|
167
|
+
import { QueryRepository } from "@alveolus/core";
|
|
168
|
+
|
|
169
|
+
import type { OrderId } from "../value-objects/order-id.identifier";
|
|
170
|
+
import type { OrderSummary } from "../views/order-summary.view";
|
|
171
|
+
|
|
172
|
+
export abstract class OrderSummaries extends QueryRepository<
|
|
173
|
+
OrderSummary
|
|
174
|
+
> {
|
|
175
|
+
abstract summaryOf(id: OrderId): Promise<OrderSummary | undefined>;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 4. Build it from storage
|
|
180
|
+
|
|
181
|
+
The adapter reads the storage and shapes the view directly: no aggregate is loaded, no rule runs.
|
|
182
|
+
|
|
183
|
+
```ts [src/ordering/driven/pg/adapters/pg-order-summaries.adapter.ts]
|
|
184
|
+
import {
|
|
185
|
+
OrderSummaries,
|
|
186
|
+
} from "../../../domain/repositories/order-summaries.repository";
|
|
187
|
+
import {
|
|
188
|
+
CustomerId,
|
|
189
|
+
} from "../../../domain/value-objects/customer-id.identifier";
|
|
190
|
+
import {
|
|
191
|
+
OrderId,
|
|
192
|
+
} from "../../../domain/value-objects/order-id.identifier";
|
|
193
|
+
import type {
|
|
194
|
+
OrderSummary,
|
|
195
|
+
} from "../../../domain/views/order-summary.view";
|
|
196
|
+
|
|
197
|
+
export class PgOrderSummaries extends OrderSummaries {
|
|
198
|
+
constructor(private readonly db: Database) {
|
|
199
|
+
super();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async summaryOf(id: OrderId): Promise<OrderSummary | undefined> {
|
|
203
|
+
const row = await this.db
|
|
204
|
+
.selectFrom("orders")
|
|
205
|
+
.where("id", "=", id.value)
|
|
206
|
+
.selectAll()
|
|
207
|
+
.executeTakeFirst();
|
|
208
|
+
if (row === undefined) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
id: new OrderId(row.id),
|
|
213
|
+
customerId: new CustomerId(row.customer_id),
|
|
214
|
+
status: row.status,
|
|
215
|
+
lineCount: row.lines.length,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 5. Return it from a query handler
|
|
222
|
+
|
|
223
|
+
A [query handler](../application/query-handlers.md) asks the query repository and returns the
|
|
224
|
+
view as it is.
|
|
225
|
+
|
|
226
|
+
```ts [src/ordering/application/queries/get-order-summary.query.ts]
|
|
227
|
+
const summary = await this.summaries.summaryOf(
|
|
228
|
+
new OrderId(orderId),
|
|
229
|
+
);
|
|
230
|
+
if (summary === undefined) {
|
|
231
|
+
return err(new OrderNotFound({ orderId }));
|
|
232
|
+
}
|
|
233
|
+
return ok(summary);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 6. Check it
|
|
237
|
+
|
|
238
|
+
Run the checks. Two rules keep the view on the query side:
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
npx alveolus arch check
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
<div class="al-cards">
|
|
245
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-foreign-command-dependency"><code>no-foreign-command-dependency</code></a></span>Only query handlers receive <code>OrderSummaries</code>.</div>
|
|
246
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-portless-adapter"><code>no-portless-adapter</code></a></span><code>PgOrderSummaries</code> extends the query repository it implements.</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
A command handler that reads it is reported:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
src/ordering/application/commands/place-order.command.ts
|
|
253
|
+
47 error tactical/no-foreign-command-dependency: The CommandHandler
|
|
254
|
+
PlaceOrderHandler receives OrderSummaries, a QueryRepository: a
|
|
255
|
+
command handler receives command repositories, ports, event
|
|
256
|
+
translators, domain services and value objects.
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## See also
|
|
260
|
+
|
|
261
|
+
- [Repositories](./repositories.md), the query repository that returns a view
|
|
262
|
+
- [Query handlers](../application/query-handlers.md), which return views
|
|
263
|
+
- [Aggregates](./aggregates.md), the write side of the same data
|
|
264
|
+
- [Published Language](../strategic/published-language.md), when a view leaves the context
|
|
265
|
+
- Rules: [`tactical/no-foreign-command-dependency`](../../rules/tactical/no-foreign-command-dependency.md)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The Domain-Driven Design building blocks of @alveolus/core as TypeScript abstract classes: aggregates, entities, value objects, domain events, repositories."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Building blocks
|
|
6
|
+
|
|
7
|
+
`@alveolus/core` gives you the building blocks of Domain-Driven Design as abstract classes. Your
|
|
8
|
+
classes extend them: `Order extends AggregateRoot`, `Money extends ValueObject`. The class says what
|
|
9
|
+
it is, and [`alveolus arch check`](../rules/index.md) knows where it belongs and what it may depend
|
|
10
|
+
on.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { AggregateRoot, err, ok, type Result } from "@alveolus/core";
|
|
14
|
+
|
|
15
|
+
export class Order extends AggregateRoot<OrderId, OrderPlaced, OrderSnapshot> {
|
|
16
|
+
place(
|
|
17
|
+
eventId: string,
|
|
18
|
+
now: Date,
|
|
19
|
+
): Result<void, OrderAlreadyPlaced | EmptyOrder> {
|
|
20
|
+
if (this.isPlaced) {
|
|
21
|
+
return err(new OrderAlreadyPlaced());
|
|
22
|
+
}
|
|
23
|
+
if (this.lines.length === 0) {
|
|
24
|
+
return err(new EmptyOrder());
|
|
25
|
+
}
|
|
26
|
+
this.status = "placed";
|
|
27
|
+
this.record(
|
|
28
|
+
new OrderPlaced({
|
|
29
|
+
id: eventId,
|
|
30
|
+
aggregateId: this.id,
|
|
31
|
+
occurredAt: now,
|
|
32
|
+
payload: { customerId: this.customerId.value },
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
return ok();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Strategic
|
|
41
|
+
|
|
42
|
+
How bounded contexts meet without sharing a model. See the [overview](./strategic/index.md) and the
|
|
43
|
+
[bounded contexts](../guide/project-layout.md#bounded-contexts) of the project layout.
|
|
44
|
+
|
|
45
|
+
| Building block | What it is |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| [Published Language](./strategic/published-language.md) | The JSON format exchanged between contexts. |
|
|
48
|
+
| [Open host services](./strategic/open-host-services.md) | The documented entry point of a context, the only class others may import. |
|
|
49
|
+
| [Anti-corruption layers](./strategic/anti-corruption-layers.md) | The adapter that reads another context and translates it into yours. |
|
|
50
|
+
|
|
51
|
+
## Domain
|
|
52
|
+
|
|
53
|
+
The model and what it needs from the outside world. Everything here lives in `domain/` and imports
|
|
54
|
+
nothing but the domain. See the [overview](./domain/index.md).
|
|
55
|
+
|
|
56
|
+
| Building block | What it is |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| [Aggregates](./domain/aggregates.md) | A cluster of objects changed as one unit, through its root, which records domain events. |
|
|
59
|
+
| [Entities](./domain/entities.md) | An object defined by its identity, inside an aggregate. |
|
|
60
|
+
| [Value objects](./domain/value-objects.md) | An immutable value compared by its attributes, and the typed identifiers. |
|
|
61
|
+
| [Domain events](./domain/domain-events.md) | Something that happened in the domain, in the past tense. |
|
|
62
|
+
| [Domain errors](./domain/domain-errors.md) | An expected business failure, returned as a value. |
|
|
63
|
+
| [Domain services](./domain/domain-services.md) | A stateless operation that belongs to no single object. |
|
|
64
|
+
| [Ports](./domain/ports.md) | What the domain needs from the outside world, in its own words. |
|
|
65
|
+
| [Repositories](./domain/repositories.md) | How aggregates are loaded and saved, and how views are read. |
|
|
66
|
+
| [Views](./domain/views.md) | What a query returns. |
|
|
67
|
+
|
|
68
|
+
## Application
|
|
69
|
+
|
|
70
|
+
The use cases, and the contracts that make them atomic and reliable. Everything here lives in
|
|
71
|
+
`application/`, except the adapters that implement the contracts. See the
|
|
72
|
+
[overview](./application/index.md).
|
|
73
|
+
|
|
74
|
+
| Building block | What it is |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| [Command handlers](./application/command-handlers.md) | The application service of one use case that changes the system. |
|
|
77
|
+
| [Query handlers](./application/query-handlers.md) | The application service of one read. |
|
|
78
|
+
| [Event translators](./application/event-translators.md) | Turns domain events into integration events. |
|
|
79
|
+
| [Integration events](./application/integration-events.md) | What other contexts receive when something happens: JSON. |
|
|
80
|
+
| [Event publishers](./application/event-publishers.md) | Sends integration events to the rest of the system. |
|
|
81
|
+
| [Unit of Work](./application/unit-of-work.md) | Makes a use case atomic: commit on success, roll back otherwise. |
|
|
82
|
+
| [Outbox](./application/outbox.md) | Stores integration events with the change, then relays them, so none is lost. |
|
|
83
|
+
|
|
84
|
+
## Utilities
|
|
85
|
+
|
|
86
|
+
| Building block | What it is |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| [Result](./utilities/result.md) | Success or failure as a value, with the helpers to combine them. |
|
|
89
|
+
|
|
90
|
+
## Principles
|
|
91
|
+
|
|
92
|
+
- **You extend, nothing is configured.** No decorator, no registry, no reflection: the class you
|
|
93
|
+
extend says what your class is. A class extending your own base class counts too.
|
|
94
|
+
- **Business errors are values.** An expected failure is a `DomainError` returned in a `Result`,
|
|
95
|
+
never thrown. Each signature lists what can go wrong. Exceptions stay in adapters.
|
|
96
|
+
- **The domain never reads the clock nor generates ids.** Dates and ids are parameters of business
|
|
97
|
+
methods; `Clock` and `IdGenerator` give them to the application.
|
|
98
|
+
- **No infrastructure.** No bus, no container, no ORM. Repositories, ports and publishers are
|
|
99
|
+
abstract classes your adapters extend with the tools you already use. Handlers take them in
|
|
100
|
+
their constructor: wire them by hand or with any container, where the same classes are the
|
|
101
|
+
injection tokens.
|
|
102
|
+
- **No runtime dependency.** `@alveolus/core` ends up in your domain and brings nothing with it.
|
|
103
|
+
|
|
104
|
+
## Import paths
|
|
105
|
+
|
|
106
|
+
Import everything from `@alveolus/core`, or each building block from its own entry point, named
|
|
107
|
+
after its page: `@alveolus/core/aggregates`, `@alveolus/core/value-objects`,
|
|
108
|
+
`@alveolus/core/command-handlers`, `@alveolus/core/result`… Both give the same classes.
|