@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.
Files changed (60) hide show
  1. package/README.md +6 -0
  2. package/dist/bin.mjs +4 -2
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/{cli-P5PwH9OE.mjs → docs-DsQHpTtV.mjs} +190 -5
  5. package/dist/docs-DsQHpTtV.mjs.map +1 -0
  6. package/dist/index.d.mts +44 -2
  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 +105 -0
  35. package/docs/guide/getting-started.md +275 -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 +183 -0
  42. package/docs/rules/layers/no-driving-shortcut.md +119 -0
  43. package/docs/rules/layers/no-impure-domain.md +189 -0
  44. package/docs/rules/layers/no-outward-import.md +184 -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 +111 -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 +106 -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,617 @@
1
+ ---
2
+ description: "Command handlers in CQRS with TypeScript: run one use case that changes the system by loading an aggregate, calling one of its methods and saving it."
3
+ ---
4
+
5
+ # Command handlers
6
+
7
+ A command handler runs one use case that changes the system: it loads an aggregate, calls one of its
8
+ methods and saves it.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Layer</dt><dd>Application</dd>
12
+ <dt>File</dt><dd><code>application/commands/place-order.command.ts</code></dd>
13
+ <dt>Extends</dt><dd><a href="#api"><code>CommandHandler&lt;Input, Output, Error&gt;</code></a></dd>
14
+ <dt>Called by</dt><dd>Driving adapters: controllers, message consumers, scripts</dd>
15
+ <dt>Checked by</dt><dd><a href="/rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a>, <a href="/rules/tactical/no-foreign-command-dependency"><code>tactical/no-foreign-command-dependency</code></a>, <a href="/rules/layers/no-outward-import"><code>layers/no-outward-import</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ Placing an order takes more than calling `order.place()`: load the order, get an id and the time,
21
+ save it, hand over its events. If the HTTP controller does all that, the message consumer and the
22
+ admin script do it again, each a little differently. Sooner or later one of them forgets to save
23
+ the events, or checks a business rule the aggregate never sees.
24
+
25
+ ::: tip The fix
26
+ A command handler does these steps once, in a plain class that knows no framework. Every entry
27
+ point calls it. The handler coordinates and the [aggregate](../domain/aggregates.md) decides, so the
28
+ rules stay in one place.
29
+ :::
30
+
31
+ ## How it works
32
+
33
+ A command handler receives a command, the data of one request, and returns a
34
+ [`Result`](../utilities/result.md). In between, it follows four steps:
35
+
36
+ <div class="al-cards al-cards-2">
37
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Load</span>Find the aggregate through its <a href="/core/domain/repositories">command repository</a>. When it is missing, return a domain error.</div>
38
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Call</span>Call one business method, with the ids and the date from the <code>IdGenerator</code> and <code>Clock</code> <a href="/core/domain/ports">ports</a>. A failure is returned as is.</div>
39
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Save</span>Store the aggregate through the same repository.</div>
40
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span>Hand over the events</span>Translate the recorded events and add them to the <a href="/core/application/outbox">outbox</a>, in the same <a href="/core/application/unit-of-work">unit of work</a>.</div>
41
+ </div>
42
+
43
+ ```ts
44
+ async handle({
45
+ orderId,
46
+ }: PlaceOrder): Promise<Result<void, PlaceOrderError>> {
47
+ const order = await this.orders.findById(new OrderId(orderId));
48
+ if (order === undefined) {
49
+ return err(new OrderNotFound({ orderId }));
50
+ }
51
+ const placed = order.place(this.ids.next(), this.clock.now());
52
+ if (!placed.ok) {
53
+ return placed;
54
+ }
55
+ await this.orders.save(order);
56
+ return ok();
57
+ }
58
+ ```
59
+
60
+ ## Where it fits
61
+
62
+ The command handler sits between the outside world and the domain. A driving adapter builds the
63
+ command and calls `handle`; the handler talks to the domain only through repositories, ports and
64
+ the aggregate.
65
+
66
+ <div class="al-diagram">
67
+ <svg viewBox="0 0 680 300" role="img" aria-label="A controller calls the PlaceOrderHandler, which in one unit of work loads the Order from the repository, calls order.place, saves the order and adds its events to the outbox.">
68
+ <defs>
69
+ <marker id="command-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
70
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
71
+ </marker>
72
+ </defs>
73
+ <rect class="box" x="8" y="122" width="130" height="56" rx="8" />
74
+ <text class="label" x="73" y="146" text-anchor="middle">Controller</text>
75
+ <text class="note" x="73" y="166" text-anchor="middle">driving adapter</text>
76
+ <path class="link" d="M 138 150 L 178 150" marker-end="url(#command-flow-arrow)" />
77
+ <rect class="boundary" x="180" y="122" width="180" height="56" rx="8" />
78
+ <text class="label" x="270" y="146" text-anchor="middle">PlaceOrderHandler</text>
79
+ <text class="note" x="270" y="166" text-anchor="middle">this page</text>
80
+ <text class="note" x="270" y="204" text-anchor="middle">one unit of work</text>
81
+ <rect class="box" x="440" y="24" width="232" height="48" rx="8" />
82
+ <text class="label" x="556" y="44" text-anchor="middle">1 · orders.findById(id)</text>
83
+ <text class="note" x="556" y="62" text-anchor="middle">command repository</text>
84
+ <rect class="box" x="440" y="92" width="232" height="48" rx="8" />
85
+ <text class="label" x="556" y="112" text-anchor="middle">2 · order.place(…)</text>
86
+ <text class="note" x="556" y="130" text-anchor="middle">the aggregate decides</text>
87
+ <rect class="box" x="440" y="160" width="232" height="48" rx="8" />
88
+ <text class="label" x="556" y="180" text-anchor="middle">3 · orders.save(order)</text>
89
+ <text class="note" x="556" y="198" text-anchor="middle">command repository</text>
90
+ <rect class="box" x="440" y="228" width="232" height="48" rx="8" />
91
+ <text class="label" x="556" y="248" text-anchor="middle">4 · outbox.add(events)</text>
92
+ <text class="note" x="556" y="266" text-anchor="middle">translated events</text>
93
+ <path class="link" d="M 360 150 L 438 48" marker-end="url(#command-flow-arrow)" />
94
+ <path class="link" d="M 360 150 L 438 116" marker-end="url(#command-flow-arrow)" />
95
+ <path class="link" d="M 360 150 L 438 184" marker-end="url(#command-flow-arrow)" />
96
+ <path class="link" d="M 360 150 L 438 252" marker-end="url(#command-flow-arrow)" />
97
+ </svg>
98
+ </div>
99
+
100
+ ::: tip
101
+ The controller turns HTTP into a command and a `Result` into a response. The handler turns a command
102
+ into calls to the domain. Neither holds a business rule.
103
+ :::
104
+
105
+ ## API
106
+
107
+ ```ts
108
+ import { CommandHandler } from "@alveolus/core";
109
+ // or: import { CommandHandler } from "@alveolus/core/command-handlers";
110
+ ```
111
+
112
+ ### Type parameters
113
+
114
+ ```ts
115
+ abstract class CommandHandler<
116
+ Input,
117
+ Output = void,
118
+ Error extends AnyDomainError = never,
119
+ > { … }
120
+ ```
121
+
122
+ | Parameter | What it is | Constraint |
123
+ | --- | --- | --- |
124
+ | `Input` | The command: the data the handler needs. | any type |
125
+ | `Output` | What a success returns, such as the id of what was created. | `void` by default |
126
+ | `Error` | The union of the [domain errors](../domain/domain-errors.md) it may return. | extends `DomainError`; `never` by default |
127
+
128
+ ### `constructor(…)` <Badge type="tip" text="you implement it" />
129
+
130
+ ```ts
131
+ constructor(
132
+ private readonly orders: Orders,
133
+ private readonly clock: Clock,
134
+ private readonly ids: IdGenerator,
135
+ ) {
136
+ super();
137
+ }
138
+ ```
139
+
140
+ `CommandHandler` declares no constructor: yours takes the dependencies as abstract classes, such
141
+ as repositories, ports, the unit of work and the outbox, and calls `super()`.
142
+
143
+ ### `handle(command)` <Badge type="info" text="abstract" /> <Badge type="tip" text="you implement it" /> <Badge type="tip" text="called by a driving adapter" />
144
+
145
+ ```ts
146
+ abstract handle(command: Input): Promise<Result<Output, Error>>
147
+ ```
148
+
149
+ Runs the use case and returns its outcome. The driving adapter that calls it turns the `Result`
150
+ into a response.
151
+
152
+ ::: warning Caveats
153
+ - `Error` only accepts `DomainError` subclasses. A technical failure, such as a lost database
154
+ connection, is thrown and handled like any other exception.
155
+ - Alveolus provides no bus and no container: wire handlers in the composition root, by hand or
156
+ with the container of your framework. See [Integrations](../../integrations/index.md).
157
+ - The events recorded by the aggregate stay on it after `save`: hand them over through the
158
+ [outbox](./outbox.md). A handler never receives an `EventPublisher`: the relay publishes.
159
+ - A command changes one aggregate. When a second one must change too, it reacts to the event of
160
+ the first, in its own transaction (Vernon, *DDD Distilled*, chapter 5). The rules do not check
161
+ this: a handler that receives two command repositories is a review item.
162
+ :::
163
+
164
+ ## Usage
165
+
166
+ Build the handler that places an order, one responsibility at a time. Each step shows the whole file: added lines are highlighted, replaced lines are struck out.
167
+
168
+ <div class="al-cards">
169
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-declare-the-command">Declare the command</a></span>Say what the caller provides and what can fail.</div>
170
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-the-handler">Declare the handler</a></span>One class, one use case.</div>
171
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-call-the-aggregate">Call the aggregate</a></span>Let the aggregate decide.</div>
172
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-save-it">Save it</a></span>Store the changed aggregate.</div>
173
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-hand-over-its-events">Hand over its events</a></span>Translate and add them to the outbox.</div>
174
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">6</span><a href="#_6-make-it-atomic">Make it atomic</a></span>One transaction for the change and its events.</div>
175
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">7</span><a href="#_7-call-it-from-a-driving-adapter">Call it from a driving adapter</a></span>Turn the Result into a response.</div>
176
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">8</span><a href="#_8-check-it">Check it</a></span>Let the rules keep it that way.</div>
177
+ </div>
178
+
179
+ ### 1. Declare the command
180
+
181
+ So that a driving adapter knows what to provide and what to handle, the command is a plain type named in the imperative, next to the union of every failure it can return.
182
+
183
+ ```ts [src/ordering/application/commands/place-order.command.ts]
184
+ import type { EmptyOrder } from "../../domain/errors/empty-order.error";
185
+ import type {
186
+ OrderAlreadyPlaced,
187
+ } from "../../domain/errors/order-already-placed.error";
188
+ import { OrderNotFound } from "../../domain/errors/order-not-found.error";
189
+
190
+ export interface PlaceOrder {
191
+ readonly orderId: string;
192
+ }
193
+
194
+ export type PlaceOrderError =
195
+ | OrderNotFound
196
+ | OrderAlreadyPlaced
197
+ | EmptyOrder;
198
+ ```
199
+
200
+ ### 2. Declare the handler
201
+
202
+ The handler extends `CommandHandler` with the command, its output and its errors. It receives its dependencies as abstract classes, so that any framework can build it, and loads the aggregate through its repository. A missing order is a failure it declares, not an exception.
203
+
204
+ ```ts [src/ordering/application/commands/place-order.command.ts]
205
+ import { CommandHandler, err, ok, type Result } from "@alveolus/core"; // [!code ++]
206
+
207
+ import type { EmptyOrder } from "../../domain/errors/empty-order.error";
208
+ import type {
209
+ OrderAlreadyPlaced,
210
+ } from "../../domain/errors/order-already-placed.error";
211
+ import { OrderNotFound } from "../../domain/errors/order-not-found.error";
212
+ import { Orders } from "../../domain/repositories/orders.repository"; // [!code ++]
213
+ import { OrderId } from "../../domain/value-objects/order-id.identifier"; // [!code ++]
214
+
215
+ export interface PlaceOrder {
216
+ readonly orderId: string;
217
+ }
218
+
219
+ export type PlaceOrderError =
220
+ | OrderNotFound
221
+ | OrderAlreadyPlaced
222
+ | EmptyOrder;
223
+
224
+ export class PlaceOrderHandler extends CommandHandler< // [!code ++]
225
+ PlaceOrder, // [!code ++]
226
+ void, // [!code ++]
227
+ PlaceOrderError // [!code ++]
228
+ > { // [!code ++]
229
+ constructor( // [!code ++]
230
+ private readonly orders: Orders, // [!code ++]
231
+ ) { // [!code ++]
232
+ super(); // [!code ++]
233
+ } // [!code ++]
234
+
235
+ async handle({ // [!code ++]
236
+ orderId, // [!code ++]
237
+ }: PlaceOrder): Promise<Result<void, PlaceOrderError>> { // [!code ++]
238
+ const order = await this.orders.findById( // [!code ++]
239
+ new OrderId(orderId), // [!code ++]
240
+ ); // [!code ++]
241
+ if (order === undefined) { // [!code ++]
242
+ return err(new OrderNotFound({ orderId })); // [!code ++]
243
+ } // [!code ++]
244
+ return ok(); // [!code ++]
245
+ } // [!code ++]
246
+ } // [!code ++]
247
+ ```
248
+
249
+ ### 3. Call the aggregate
250
+
251
+ The handler decides nothing: it calls one business method and returns its failure as is. The event id and the date come from the `Clock` and `IdGenerator` [ports](../domain/ports.md), so the aggregate never reads them itself.
252
+
253
+ ```ts [src/ordering/application/commands/place-order.command.ts]
254
+ import { CommandHandler, err, ok, type Result } from "@alveolus/core"; // [!code --]
255
+ import { // [!code ++]
256
+ Clock, // [!code ++]
257
+ CommandHandler, // [!code ++]
258
+ err, // [!code ++]
259
+ IdGenerator, // [!code ++]
260
+ ok, // [!code ++]
261
+ type Result, // [!code ++]
262
+ } from "@alveolus/core"; // [!code ++]
263
+
264
+ import type { EmptyOrder } from "../../domain/errors/empty-order.error";
265
+ import type {
266
+ OrderAlreadyPlaced,
267
+ } from "../../domain/errors/order-already-placed.error";
268
+ import { OrderNotFound } from "../../domain/errors/order-not-found.error";
269
+ import { Orders } from "../../domain/repositories/orders.repository";
270
+ import { OrderId } from "../../domain/value-objects/order-id.identifier";
271
+
272
+ export interface PlaceOrder {
273
+ readonly orderId: string;
274
+ }
275
+
276
+ export type PlaceOrderError =
277
+ | OrderNotFound
278
+ | OrderAlreadyPlaced
279
+ | EmptyOrder;
280
+
281
+ export class PlaceOrderHandler extends CommandHandler<
282
+ PlaceOrder,
283
+ void,
284
+ PlaceOrderError
285
+ > {
286
+ constructor(
287
+ private readonly orders: Orders,
288
+ private readonly clock: Clock, // [!code ++]
289
+ private readonly ids: IdGenerator, // [!code ++]
290
+ ) {
291
+ super();
292
+ }
293
+
294
+ async handle({
295
+ orderId,
296
+ }: PlaceOrder): Promise<Result<void, PlaceOrderError>> {
297
+ const order = await this.orders.findById(
298
+ new OrderId(orderId),
299
+ );
300
+ if (order === undefined) {
301
+ return err(new OrderNotFound({ orderId }));
302
+ }
303
+ const placed = order.place( // [!code ++]
304
+ this.ids.next(), // [!code ++]
305
+ this.clock.now(), // [!code ++]
306
+ ); // [!code ++]
307
+ if (!placed.ok) { // [!code ++]
308
+ return placed; // [!code ++]
309
+ } // [!code ++]
310
+ return ok();
311
+ }
312
+ }
313
+ ```
314
+
315
+ ### 4. Save it
316
+
317
+ Only a successful change is saved: when `place` fails, the handler has already returned and the order stays as it was in storage.
318
+
319
+ ```ts [src/ordering/application/commands/place-order.command.ts]
320
+ import {
321
+ Clock,
322
+ CommandHandler,
323
+ err,
324
+ IdGenerator,
325
+ ok,
326
+ type Result,
327
+ } from "@alveolus/core";
328
+
329
+ import type { EmptyOrder } from "../../domain/errors/empty-order.error";
330
+ import type {
331
+ OrderAlreadyPlaced,
332
+ } from "../../domain/errors/order-already-placed.error";
333
+ import { OrderNotFound } from "../../domain/errors/order-not-found.error";
334
+ import { Orders } from "../../domain/repositories/orders.repository";
335
+ import { OrderId } from "../../domain/value-objects/order-id.identifier";
336
+
337
+ export interface PlaceOrder {
338
+ readonly orderId: string;
339
+ }
340
+
341
+ export type PlaceOrderError =
342
+ | OrderNotFound
343
+ | OrderAlreadyPlaced
344
+ | EmptyOrder;
345
+
346
+ export class PlaceOrderHandler extends CommandHandler<
347
+ PlaceOrder,
348
+ void,
349
+ PlaceOrderError
350
+ > {
351
+ constructor(
352
+ private readonly orders: Orders,
353
+ private readonly clock: Clock,
354
+ private readonly ids: IdGenerator,
355
+ ) {
356
+ super();
357
+ }
358
+
359
+ async handle({
360
+ orderId,
361
+ }: PlaceOrder): Promise<Result<void, PlaceOrderError>> {
362
+ const order = await this.orders.findById(
363
+ new OrderId(orderId),
364
+ );
365
+ if (order === undefined) {
366
+ return err(new OrderNotFound({ orderId }));
367
+ }
368
+ const placed = order.place(
369
+ this.ids.next(),
370
+ this.clock.now(),
371
+ );
372
+ if (!placed.ok) {
373
+ return placed;
374
+ }
375
+ await this.orders.save(order); // [!code ++]
376
+ return ok();
377
+ }
378
+ }
379
+ ```
380
+
381
+ ### 5. Hand over its events
382
+
383
+ Other contexts must learn that the order was placed. After saving, the handler pulls the recorded events, translates each one into the published language and adds them to the [outbox](./outbox.md).
384
+
385
+ ```ts [src/ordering/application/commands/place-order.command.ts]
386
+ import {
387
+ Clock,
388
+ CommandHandler,
389
+ err,
390
+ IdGenerator,
391
+ ok,
392
+ Outbox, // [!code ++]
393
+ type Result,
394
+ } from "@alveolus/core";
395
+
396
+ import type { EmptyOrder } from "../../domain/errors/empty-order.error";
397
+ import type {
398
+ OrderAlreadyPlaced,
399
+ } from "../../domain/errors/order-already-placed.error";
400
+ import { OrderNotFound } from "../../domain/errors/order-not-found.error";
401
+ import { Orders } from "../../domain/repositories/orders.repository";
402
+ import { OrderId } from "../../domain/value-objects/order-id.identifier";
403
+ import { // [!code ++]
404
+ OrderEventsTranslator, // [!code ++]
405
+ } from "../translators/order-events.translator"; // [!code ++]
406
+
407
+ export interface PlaceOrder {
408
+ readonly orderId: string;
409
+ }
410
+
411
+ export type PlaceOrderError =
412
+ | OrderNotFound
413
+ | OrderAlreadyPlaced
414
+ | EmptyOrder;
415
+
416
+ export class PlaceOrderHandler extends CommandHandler<
417
+ PlaceOrder,
418
+ void,
419
+ PlaceOrderError
420
+ > {
421
+ constructor(
422
+ private readonly orders: Orders,
423
+ private readonly outbox: Outbox, // [!code ++]
424
+ private readonly translator: OrderEventsTranslator, // [!code ++]
425
+ private readonly clock: Clock,
426
+ private readonly ids: IdGenerator,
427
+ ) {
428
+ super();
429
+ }
430
+
431
+ async handle({
432
+ orderId,
433
+ }: PlaceOrder): Promise<Result<void, PlaceOrderError>> {
434
+ const order = await this.orders.findById(
435
+ new OrderId(orderId),
436
+ );
437
+ if (order === undefined) {
438
+ return err(new OrderNotFound({ orderId }));
439
+ }
440
+ const placed = order.place(
441
+ this.ids.next(),
442
+ this.clock.now(),
443
+ );
444
+ if (!placed.ok) {
445
+ return placed;
446
+ }
447
+ await this.orders.save(order);
448
+ const events = order // [!code ++]
449
+ .pullDomainEvents() // [!code ++]
450
+ .map((event) => // [!code ++]
451
+ this.translator.translate(event, { // [!code ++]
452
+ correlationId: orderId, // [!code ++]
453
+ }), // [!code ++]
454
+ ); // [!code ++]
455
+ await this.outbox.add(events); // [!code ++]
456
+ return ok();
457
+ }
458
+ }
459
+ ```
460
+
461
+ ### 6. Make it atomic
462
+
463
+ If saving the order succeeds and adding its events fails, the rest of the system never hears of it. The [unit of work](./unit-of-work.md) runs both in one transaction, and rolls back when the work returns a failure.
464
+
465
+ ```ts [src/ordering/application/commands/place-order.command.ts]
466
+ import {
467
+ Clock,
468
+ CommandHandler,
469
+ err,
470
+ IdGenerator,
471
+ ok,
472
+ Outbox,
473
+ type Result,
474
+ UnitOfWork, // [!code ++]
475
+ } from "@alveolus/core";
476
+
477
+ import type { EmptyOrder } from "../../domain/errors/empty-order.error";
478
+ import type {
479
+ OrderAlreadyPlaced,
480
+ } from "../../domain/errors/order-already-placed.error";
481
+ import { OrderNotFound } from "../../domain/errors/order-not-found.error";
482
+ import { Orders } from "../../domain/repositories/orders.repository";
483
+ import { OrderId } from "../../domain/value-objects/order-id.identifier";
484
+ import {
485
+ OrderEventsTranslator,
486
+ } from "../translators/order-events.translator";
487
+
488
+ export interface PlaceOrder {
489
+ readonly orderId: string;
490
+ }
491
+
492
+ export type PlaceOrderError =
493
+ | OrderNotFound
494
+ | OrderAlreadyPlaced
495
+ | EmptyOrder;
496
+
497
+ export class PlaceOrderHandler extends CommandHandler<
498
+ PlaceOrder,
499
+ void,
500
+ PlaceOrderError
501
+ > {
502
+ constructor(
503
+ private readonly orders: Orders,
504
+ private readonly unitOfWork: UnitOfWork, // [!code ++]
505
+ private readonly outbox: Outbox,
506
+ private readonly translator: OrderEventsTranslator,
507
+ private readonly clock: Clock,
508
+ private readonly ids: IdGenerator,
509
+ ) {
510
+ super();
511
+ }
512
+
513
+ async handle({
514
+ orderId,
515
+ }: PlaceOrder): Promise<Result<void, PlaceOrderError>> {
516
+ const order = await this.orders.findById( // [!code --]
517
+ new OrderId(orderId), // [!code --]
518
+ ); // [!code --]
519
+ if (order === undefined) { // [!code --]
520
+ return err(new OrderNotFound({ orderId })); // [!code --]
521
+ } // [!code --]
522
+ const placed = order.place( // [!code --]
523
+ this.ids.next(), // [!code --]
524
+ this.clock.now(), // [!code --]
525
+ ); // [!code --]
526
+ if (!placed.ok) { // [!code --]
527
+ return placed; // [!code --]
528
+ } // [!code --]
529
+ await this.orders.save(order); // [!code --]
530
+ const events = order // [!code --]
531
+ .pullDomainEvents() // [!code --]
532
+ .map((event) => // [!code --]
533
+ this.translator.translate(event, { // [!code --]
534
+ correlationId: orderId, // [!code --]
535
+ }), // [!code --]
536
+ return this.unitOfWork.run(async () => { // [!code ++]
537
+ const order = await this.orders.findById( // [!code ++]
538
+ new OrderId(orderId), // [!code ++]
539
+ );
540
+ await this.outbox.add(events); // [!code --]
541
+ return ok(); // [!code --]
542
+ if (order === undefined) { // [!code ++]
543
+ return err(new OrderNotFound({ orderId })); // [!code ++]
544
+ } // [!code ++]
545
+ const placed = order.place( // [!code ++]
546
+ this.ids.next(), // [!code ++]
547
+ this.clock.now(), // [!code ++]
548
+ ); // [!code ++]
549
+ if (!placed.ok) { // [!code ++]
550
+ return placed; // [!code ++]
551
+ } // [!code ++]
552
+ await this.orders.save(order); // [!code ++]
553
+ const events = order // [!code ++]
554
+ .pullDomainEvents() // [!code ++]
555
+ .map((event) => // [!code ++]
556
+ this.translator.translate(event, { // [!code ++]
557
+ correlationId: orderId, // [!code ++]
558
+ }), // [!code ++]
559
+ ); // [!code ++]
560
+ await this.outbox.add(events); // [!code ++]
561
+ return ok(); // [!code ++]
562
+ }); // [!code ++]
563
+ }
564
+ }
565
+ ```
566
+
567
+ This is the complete handler.
568
+
569
+ ### 7. Call it from a driving adapter
570
+
571
+ So that HTTP stays out of the application, a controller builds the command, calls `handle` and turns the `Result` into a response. Domain errors become HTTP errors there, and nowhere else.
572
+
573
+ ```ts [src/ordering/driving/http/controllers/orders.controller.ts]
574
+ const placed = await this.placeOrder.handle({ orderId });
575
+ if (!placed.ok) {
576
+ return {
577
+ status: 422,
578
+ body: {
579
+ error: placed.error.type,
580
+ details: placed.error.payload,
581
+ },
582
+ };
583
+ }
584
+ return { status: 204 };
585
+ ```
586
+
587
+ ### 8. Check it
588
+
589
+ Run the checks. Three rules keep the handler the way it is now:
590
+
591
+ ```sh
592
+ npx alveolus arch check
593
+ ```
594
+
595
+ <div class="al-cards">
596
+ <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>It receives command repositories, ports, event translators, domain services and value objects: never a query repository or another handler.</div>
597
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-misplaced-class"><code>no-misplaced-class</code></a></span>It stays alone in <code>application/commands/*.command.ts</code>.</div>
598
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-outward-import"><code>no-outward-import</code></a></span>It imports the domain and the application, never an adapter.</div>
599
+ </div>
600
+
601
+ A query repository added to its constructor is reported:
602
+
603
+ ```
604
+ src/ordering/application/commands/place-order.command.ts
605
+ 46 error tactical/no-foreign-command-dependency: The CommandHandler
606
+ PlaceOrderHandler receives OrderSummaries, a QueryRepository: a
607
+ command handler receives command repositories, ports, event
608
+ translators, domain services and value objects.
609
+ ```
610
+
611
+ ## See also
612
+
613
+ - [Aggregates](../domain/aggregates.md), which hold the rules the handler calls
614
+ - [Repositories](../domain/repositories.md), to load and save aggregates
615
+ - [Unit of Work](./unit-of-work.md) and [Outbox](./outbox.md), to change and record atomically
616
+ - [Query handlers](./query-handlers.md), for requests that only read
617
+ - Rules: [`tactical/no-foreign-command-dependency`](../../rules/tactical/no-foreign-command-dependency.md), [`layers/no-outward-import`](../../rules/layers/no-outward-import.md), [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md)