@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,249 @@
1
+ ---
2
+ description: "Domain services in Domain-Driven Design with TypeScript: stateless classes that hold a business rule no single aggregate or value object owns."
3
+ ---
4
+
5
+ # Domain Services
6
+
7
+ A domain service is a stateless class of the domain that holds a business rule no single object
8
+ owns.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Layer</dt><dd>Domain</dd>
12
+ <dt>File</dt><dd><code>domain/services/order-limit.service.ts</code></dd>
13
+ <dt>Extends</dt><dd><a href="#api"><code>DomainService</code></a></dd>
14
+ <dt>Called by</dt><dd><a href="/core/application/command-handlers">Command handlers</a></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-loose-code"><code>tactical/no-loose-code</code></a>, <a href="/rules/layers/no-impure-domain"><code>layers/no-impure-domain</code></a>, <a href="/rules/tactical/no-stateful-service"><code>tactical/no-stateful-service</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ A new customer may not place an order of more than 5 lines. The rule needs the customer and the
21
+ order. Put it in `Order`, and the order must hold the `Customer`: two aggregates merge. Put it in
22
+ the command handler, and the rule leaves the domain, copied in every handler that places orders.
23
+
24
+ ::: tip The fix
25
+ A domain service holds the rule: `OrderLimit` takes the order and the customer, and answers with a
26
+ `Result`. The rule stays in the domain, in one place, and neither aggregate holds the other.
27
+ :::
28
+
29
+ ## How it works
30
+
31
+ A domain service is a plain function of the domain, written as a class: it keeps nothing between
32
+ calls. It never loads, saves or publishes: the command handler does that, and hands the service
33
+ what it needs.
34
+
35
+ <div class="al-cards">
36
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Receive</span>The aggregates and values the rule needs, as parameters.</div>
37
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Decide</span>Apply the rule, in the words of the domain.</div>
38
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Answer</span>Return a <a href="/core/utilities/result"><code>Result</code></a>: a value, or a <a href="/core/domain/domain-errors">domain error</a>.</div>
39
+ </div>
40
+
41
+ ```ts
42
+ check(order: Order, customer: Customer): Result<void, OrderTooLarge> {
43
+ if (!customer.isNew) {
44
+ return ok();
45
+ }
46
+ if (order.lineCount <= this.maxLinesForNewCustomers) {
47
+ return ok();
48
+ }
49
+ return err(new OrderTooLarge({ limit: this.maxLinesForNewCustomers }));
50
+ }
51
+ ```
52
+
53
+ ## Where it fits
54
+
55
+ The [command handler](../application/command-handlers.md) loads both aggregates, asks the service,
56
+ and only then calls the aggregate it changes.
57
+
58
+ <div class="al-diagram">
59
+ <svg viewBox="0 0 680 380" role="img" aria-label="The PlaceOrderHandler loads the Order and the Customer, asks the OrderLimit domain service, then places the order and saves it.">
60
+ <defs>
61
+ <marker id="service-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
62
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
63
+ </marker>
64
+ </defs>
65
+ <rect class="box" x="8" y="162" width="130" height="56" rx="8" />
66
+ <text class="label" x="73" y="186" text-anchor="middle">Controller</text>
67
+ <text class="note" x="73" y="206" text-anchor="middle">driving adapter</text>
68
+ <path class="link" d="M 138 190 L 178 190" marker-end="url(#service-flow-arrow)" />
69
+ <rect class="box" x="180" y="162" width="180" height="56" rx="8" />
70
+ <text class="label" x="270" y="186" text-anchor="middle">PlaceOrderHandler</text>
71
+ <text class="note" x="270" y="206" text-anchor="middle">command handler</text>
72
+ <rect class="box" x="440" y="24" width="232" height="48" rx="8" />
73
+ <text class="label" x="556" y="44" text-anchor="middle">1 · orders.findById(…)</text>
74
+ <text class="note" x="556" y="62" text-anchor="middle">loads the Order</text>
75
+ <rect class="box" x="440" y="92" width="232" height="48" rx="8" />
76
+ <text class="label" x="556" y="112" text-anchor="middle">2 · customers.findById(…)</text>
77
+ <text class="note" x="556" y="130" text-anchor="middle">loads the Customer</text>
78
+ <rect class="boundary" x="440" y="160" width="232" height="48" rx="8" />
79
+ <text class="label" x="556" y="180" text-anchor="middle">3 · orderLimit.check(…)</text>
80
+ <text class="note" x="556" y="198" text-anchor="middle">this page: rule on both</text>
81
+ <rect class="box" x="440" y="228" width="232" height="48" rx="8" />
82
+ <text class="label" x="556" y="248" text-anchor="middle">4 · order.place(…)</text>
83
+ <text class="note" x="556" y="266" text-anchor="middle">changes the Order</text>
84
+ <rect class="box" x="440" y="296" width="232" height="48" rx="8" />
85
+ <text class="label" x="556" y="316" text-anchor="middle">5 · orders.save(order)</text>
86
+ <text class="note" x="556" y="334" text-anchor="middle">stores it</text>
87
+ <path class="link" d="M 360 190 L 438 48" marker-end="url(#service-flow-arrow)" />
88
+ <path class="link" d="M 360 190 L 438 116" marker-end="url(#service-flow-arrow)" />
89
+ <path class="link" d="M 360 190 L 438 184" marker-end="url(#service-flow-arrow)" />
90
+ <path class="link" d="M 360 190 L 438 252" marker-end="url(#service-flow-arrow)" />
91
+ <path class="link" d="M 360 190 L 438 320" marker-end="url(#service-flow-arrow)" />
92
+ </svg>
93
+ </div>
94
+
95
+ ::: tip
96
+ The service reads two aggregates but changes none of them. Only `Order` changes, in one
97
+ transaction.
98
+ :::
99
+
100
+ ## API
101
+
102
+ ```ts
103
+ import { DomainService } from "@alveolus/core";
104
+ // or: import { DomainService } from "@alveolus/core/domain-services";
105
+ ```
106
+
107
+ ### Declaration
108
+
109
+ ```ts
110
+ abstract class DomainService {}
111
+ ```
112
+
113
+ `DomainService` has no members: extending it marks the class as a domain service, for you and for
114
+ `alveolus arch check`.
115
+
116
+ ### `constructor(…)` <Badge type="info" text="optional" /> <Badge type="tip" text="you write it" />
117
+
118
+ ```ts
119
+ constructor(private readonly limit: number)
120
+ ```
121
+
122
+ Takes configuration, such as a limit or a rate, in `readonly` fields, and calls `super()`.
123
+
124
+ ### Your methods <Badge type="tip" text="called by the command handler" />
125
+
126
+ ```ts
127
+ check(order: Order, customer: Customer): Result<void, OrderTooLarge>
128
+ ```
129
+
130
+ Take domain objects and return a `Result` when the rule can refuse.
131
+
132
+ ::: warning Caveats
133
+ - A domain service is not an application service: it never loads, saves or publishes. That is the
134
+ job of a [command handler](../application/command-handlers.md).
135
+ - It may hold configuration in `readonly` fields, never an aggregate or an entity.
136
+ :::
137
+
138
+ ## Usage
139
+
140
+ Build `OrderLimit`, a rule that needs two aggregates, then call it from the handler that places an
141
+ order. Each step shows the whole file it changes: 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-find-the-rule">Find the rule</a></span>One that belongs to no aggregate.</div>
145
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-the-service">Declare the service</a></span>A home named after the rule.</div>
146
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-check-the-rule">Check the rule</a></span>Aggregates in, a Result out.</div>
147
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-call-it-from-a-handler">Call it from a handler</a></span>Load, check, then change.</div>
148
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-check-it">Check it</a></span>Let the rules keep it that way.</div>
149
+ </div>
150
+
151
+ ### 1. Find the rule
152
+
153
+ A new customer may not place an order with more than a few lines: the rule needs the
154
+ [`Order`](./aggregates.md) and the `Customer`, and belongs to neither.
155
+
156
+ ### 2. Declare the service
157
+
158
+ So that the rule has a home in the domain, it gets a class of its own, named after the rule. Its
159
+ settings come in through the constructor.
160
+
161
+ ```ts [src/ordering/domain/services/order-limit.service.ts]
162
+ import { DomainService } from "@alveolus/core";
163
+
164
+ export class OrderLimit extends DomainService {
165
+ constructor(private readonly maxLinesForNewCustomers: number) {
166
+ super();
167
+ }
168
+ }
169
+ ```
170
+
171
+ ### 3. Check the rule
172
+
173
+ The service takes the aggregates as parameters and changes neither. Like an aggregate, it returns
174
+ the failure in a `Result`.
175
+
176
+ ```ts [src/ordering/domain/services/order-limit.service.ts]
177
+ import { DomainService } from "@alveolus/core"; // [!code --]
178
+ import { DomainService, err, ok, type Result } from "@alveolus/core"; // [!code ++]
179
+
180
+ import type { Customer } from "../aggregates/customer.aggregate"; // [!code ++]
181
+ import type { Order } from "../aggregates/order.aggregate"; // [!code ++]
182
+ import { OrderTooLarge } from "../errors/order-too-large.error"; // [!code ++]
183
+
184
+ export class OrderLimit extends DomainService {
185
+ constructor(private readonly maxLinesForNewCustomers: number) {
186
+ super();
187
+ }
188
+
189
+ check( // [!code ++]
190
+ order: Order, // [!code ++]
191
+ customer: Customer, // [!code ++]
192
+ ): Result<void, OrderTooLarge> { // [!code ++]
193
+ if (!customer.isNew) { // [!code ++]
194
+ return ok(); // [!code ++]
195
+ } // [!code ++]
196
+ if (order.lineCount <= this.maxLinesForNewCustomers) { // [!code ++]
197
+ return ok(); // [!code ++]
198
+ } // [!code ++]
199
+ return err( // [!code ++]
200
+ new OrderTooLarge({ limit: this.maxLinesForNewCustomers }), // [!code ++]
201
+ ); // [!code ++]
202
+ } // [!code ++]
203
+ }
204
+ ```
205
+
206
+ ### 4. Call it from a handler
207
+
208
+ The [command handler](../application/command-handlers.md) loads the order and the customer, asks
209
+ the service, and only then changes the order. The composition root sets the limit with
210
+ `new OrderLimit(5)`.
211
+
212
+ ```ts [src/ordering/application/commands/place-order.command.ts]
213
+ const allowed = this.orderLimit.check(order, customer);
214
+ if (!allowed.ok) {
215
+ return allowed;
216
+ }
217
+ const placed = order.place(this.ids.next(), this.clock.now());
218
+ ```
219
+
220
+ ### 5. Check it
221
+
222
+ Run the checks. Three rules keep the service the way it is now:
223
+
224
+ ```sh
225
+ npx alveolus arch check
226
+ ```
227
+
228
+ <div class="al-cards">
229
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-loose-code"><code>no-loose-code</code></a></span>The rule is a method of a <code>DomainService</code>, not a free function.</div>
230
+ <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>domain/services/*.service.ts</code>.</div>
231
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-impure-domain"><code>no-impure-domain</code></a></span>It imports the domain only: no adapter, no framework.</div>
232
+ </div>
233
+
234
+ The same rule written as a function is reported:
235
+
236
+ ```
237
+ src/ordering/domain/services/order-limit.ts
238
+ 5 error tactical/no-loose-code: The function checkOrderLimit floats
239
+ outside any class: make it a method of a value object or of a
240
+ DomainService.
241
+ ```
242
+
243
+ ## See also
244
+
245
+ - [Aggregates](./aggregates.md), where most rules belong
246
+ - [Value objects](./value-objects.md), the other home for calculations
247
+ - [Command handlers](../application/command-handlers.md), which call domain services
248
+ - Rules: [`tactical/no-loose-code`](../../rules/tactical/no-loose-code.md), [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md)
249
+ - Vaughn Vernon, *Implementing Domain-Driven Design*, chapter 7, "Services"
@@ -0,0 +1,431 @@
1
+ ---
2
+ description: "Entities in Domain-Driven Design with TypeScript: objects inside an aggregate that keep their identity while their attributes change."
3
+ ---
4
+
5
+ # Entities
6
+
7
+ An entity is an object inside an aggregate that keeps its identity while its attributes change.
8
+
9
+ <dl class="al-glance">
10
+ <dt>Layer</dt><dd>Domain</dd>
11
+ <dt>File</dt><dd><code>domain/entities/order-line.entity.ts</code></dd>
12
+ <dt>Extends</dt><dd><a href="#api"><code>Entity&lt;Id, Snapshot&gt;</code></a></dd>
13
+ <dt>Called by</dt><dd>The root of its <a href="/core/domain/aggregates">aggregate</a>, and nothing else</dd>
14
+ <dt>Checked by</dt><dd><a href="/rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a>, <a href="/rules/tactical/no-thrown-failure"><code>tactical/no-thrown-failure</code></a>, <a href="/rules/tactical/no-aggregate-reference"><code>tactical/no-aggregate-reference</code></a>, <a href="/rules/tactical/no-public-field"><code>tactical/no-public-field</code></a></dd>
15
+ </dl>
16
+
17
+ ## Why
18
+
19
+ An order has two lines for the same product, one of 1 and one of 3. The customer changes the second
20
+ one to 0. If the lines are plain objects with a public `quantity`, nothing refuses the 0, nothing
21
+ tells the two lines apart, and nothing stops the change once the order is placed.
22
+
23
+ ::: tip The fix
24
+ `OrderLine` is an entity: it has its own identifier, so the second line stays the second line, and
25
+ a `changeQuantity` method that refuses a quantity of 0. The `Order` root decides whether the line
26
+ may change at all.
27
+ :::
28
+
29
+ ## How it works
30
+
31
+ An entity is defined by its identity, not by its values: two lines with the same product and
32
+ quantity are still two lines. It lives inside an [aggregate](./aggregates.md), and only the root
33
+ of that aggregate holds it and calls it.
34
+
35
+ The rules are split between the two:
36
+
37
+ <div class="al-cards al-cards-2">
38
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>The entity keeps its own rules</span>"A quantity is at least 1" only involves the line: <code>OrderLine.changeQuantity</code> checks it.</div>
39
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>The root keeps the shared rules</span>"No change once placed" involves the order: <code>Order.changeLineQuantity</code> checks it, then finds the line and calls it.</div>
40
+ </div>
41
+
42
+ ```ts
43
+ changeQuantity(quantity: number): Result<void, InvalidQuantity> {
44
+ if (quantity <= 0) {
45
+ return err(new InvalidQuantity({ quantity }));
46
+ }
47
+ this.currentQuantity = quantity;
48
+ return ok();
49
+ }
50
+ ```
51
+
52
+ ## Where it fits
53
+
54
+ A [command handler](../application/command-handlers.md) never reaches an entity. It calls the root,
55
+ which calls the entity. The entity is saved inside the snapshot of its aggregate.
56
+
57
+ <div class="al-diagram">
58
+ <svg viewBox="0 0 680 150" role="img" aria-label="A command handler calls the Order root, which calls the OrderLine entity. The order line is saved inside the snapshot of the order.">
59
+ <defs>
60
+ <marker id="entity-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
61
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
62
+ </marker>
63
+ </defs>
64
+ <rect class="box" x="8" y="24" width="160" height="56" rx="8" />
65
+ <text class="label" x="88" y="48" text-anchor="middle">Command handler</text>
66
+ <text class="note" x="88" y="68" text-anchor="middle">calls the root</text>
67
+ <path class="link" d="M 168 52 L 258 52" marker-end="url(#entity-flow-arrow)" />
68
+ <rect class="box" x="260" y="24" width="160" height="56" rx="8" />
69
+ <text class="label" x="340" y="48" text-anchor="middle">Order</text>
70
+ <text class="note" x="340" y="68" text-anchor="middle">changeLineQuantity()</text>
71
+ <path class="link" d="M 420 52 L 510 52" marker-end="url(#entity-flow-arrow)" />
72
+ <rect class="boundary" x="512" y="24" width="160" height="56" rx="8" />
73
+ <text class="label" x="592" y="48" text-anchor="middle">OrderLine</text>
74
+ <text class="note" x="592" y="68" text-anchor="middle">changeQuantity()</text>
75
+ <text class="note" x="340" y="122" text-anchor="middle">saved as part of Order.toSnapshot()</text>
76
+ </svg>
77
+ </div>
78
+
79
+ ::: tip
80
+ Code outside the aggregate never holds an entity. It names a line by its `OrderLineId`, and the root
81
+ finds it.
82
+ :::
83
+
84
+ ## API
85
+
86
+ ```ts
87
+ import { Entity } from "@alveolus/core";
88
+ // or: import { Entity } from "@alveolus/core/entities";
89
+ ```
90
+
91
+ ### Type parameters
92
+
93
+ ```ts
94
+ abstract class Entity<
95
+ Id extends AnyIdentifier,
96
+ Snapshot extends AnySnapshot = AnySnapshot,
97
+ > { … }
98
+ ```
99
+
100
+ | Parameter | What it is | Constraint |
101
+ | --- | --- | --- |
102
+ | `Id` | The [identifier](./value-objects.md#identifier) of the entity. | extends `Identifier` |
103
+ | `Snapshot` | The plain data its state is saved as. | a `type` of plain data; any by default |
104
+
105
+ A snapshot holds only `SnapshotValue`s: strings, numbers, booleans, `null`, `bigint`, `Date`, and
106
+ arrays or objects of them. `AnySnapshot` is the type of any snapshot, and `AnyEntity` the type of
107
+ any entity.
108
+
109
+ ### `constructor(id)` <Badge type="info" text="protected" /> <Badge type="tip" text="you call it" />
110
+
111
+ ```ts
112
+ protected constructor(id: Id)
113
+ ```
114
+
115
+ Stores the identifier. Declare your own constructor `private` and call `super(id)` from it: only
116
+ your factories and `fromSnapshot` create the entity.
117
+
118
+ ### `toSnapshot()` <Badge type="info" text="abstract" /> <Badge type="tip" text="you implement it" />
119
+
120
+ ```ts
121
+ abstract toSnapshot(): Snapshot
122
+ ```
123
+
124
+ Returns the state as plain data, for the snapshot of the aggregate.
125
+
126
+ ### `fromSnapshot(snapshot)` <Badge type="info" text="static · convention" /> <Badge type="tip" text="you implement it" />
127
+
128
+ ```ts
129
+ static fromSnapshot(snapshot: OrderLineSnapshot): OrderLine
130
+ ```
131
+
132
+ Rebuilds the entity from its snapshot, through the private constructor, without checking rules.
133
+ Not declared by `Entity`: TypeScript has no abstract static methods.
134
+
135
+ ### `equals(other)` <Badge type="tip" text="called by the root" />
136
+
137
+ ```ts
138
+ equals(other: AnyEntity): boolean
139
+ ```
140
+
141
+ `true` when `other` is the same object, or an instance of the same class with an equal
142
+ identifier, whatever the other attributes.
143
+
144
+ ### `id` <Badge type="info" text="readonly" /> <Badge type="tip" text="read by anyone" />
145
+
146
+ ```ts
147
+ readonly id: Id
148
+ ```
149
+
150
+ The identifier given to the constructor.
151
+
152
+ ::: warning Caveats
153
+ - `equals` requires the same concrete class: an entity is never equal to an instance of a subclass
154
+ with the same identifier.
155
+ - Declare the snapshot with `type`, not `interface`: an interface does not satisfy `AnySnapshot`.
156
+ - TypeScript has no abstract static methods: the compiler does not check that `fromSnapshot`
157
+ exists.
158
+ - An entity has no `record`: only the root of the aggregate records domain events.
159
+ :::
160
+
161
+ ## Usage
162
+
163
+ Build `OrderLine`, an entity inside the `Order` aggregate, one idea at a time. Each step shows the whole file it changes: added lines are highlighted, replaced lines are struck out.
164
+
165
+ <div class="al-cards">
166
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-name-it">Name it</a></span>Give the entity its identifier.</div>
167
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-it">Declare it</a></span>One class, one way in.</div>
168
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-make-it-storable">Make it storable</a></span>A snapshot inside the order's.</div>
169
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-change-it-through-a-method">Change it through a method</a></span>No setter, a business method.</div>
170
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-expose-reads-as-getters">Expose reads as getters</a></span>Read without changing.</div>
171
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">6</span><a href="#_6-use-it-from-its-aggregate">Use it from its aggregate</a></span>Only the root holds it.</div>
172
+ <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>
173
+ </div>
174
+
175
+ ### 1. Name it
176
+
177
+ An entity is known by its identifier: declare `OrderLineId` as an
178
+ [identifier](./value-objects.md#identifier), in `domain/value-objects/order-line-id.identifier.ts`.
179
+
180
+ ### 2. Declare it
181
+
182
+ So that a line is only created through the rules of its aggregate, the constructor is private and
183
+ `create` is the only way in. As for an aggregate, `id` is passed to `super`, which stores it as
184
+ the public, read-only identifier. `currentQuantity` changes, so it is the one field that is not
185
+ `readonly`.
186
+
187
+ ```ts [src/ordering/domain/entities/order-line.entity.ts]
188
+ import { Entity } from "@alveolus/core";
189
+
190
+ import { OrderLineId } from "../value-objects/order-line-id.identifier";
191
+ import { ProductId } from "../value-objects/product-id.identifier";
192
+
193
+ export class OrderLine extends Entity<OrderLineId> {
194
+ private constructor(
195
+ id: OrderLineId,
196
+ private readonly productId: ProductId,
197
+ private currentQuantity: number,
198
+ ) {
199
+ super(id);
200
+ }
201
+
202
+ static create(id: OrderLineId, productId: ProductId): OrderLine {
203
+ return new OrderLine(id, productId, 1);
204
+ }
205
+ }
206
+ ```
207
+
208
+ TypeScript now asks for `toSnapshot()`: the next step adds it.
209
+
210
+ ### 3. Make it storable
211
+
212
+ The state is private, so the order saves it as a snapshot: plain, read-only data declared with
213
+ `type`. `fromSnapshot` rebuilds the line without checking any rule.
214
+
215
+ ```ts [src/ordering/domain/entities/order-line.entity.ts]
216
+ import { Entity } from "@alveolus/core";
217
+
218
+ import { OrderLineId } from "../value-objects/order-line-id.identifier";
219
+ import { ProductId } from "../value-objects/product-id.identifier";
220
+
221
+ export type OrderLineSnapshot = { // [!code ++]
222
+ readonly id: string; // [!code ++]
223
+ readonly productId: string; // [!code ++]
224
+ readonly quantity: number; // [!code ++]
225
+ }; // [!code ++]
226
+
227
+ export class OrderLine extends Entity<OrderLineId> { // [!code --]
228
+ export class OrderLine extends Entity<OrderLineId, OrderLineSnapshot> { // [!code ++]
229
+ private constructor(
230
+ id: OrderLineId,
231
+ private readonly productId: ProductId,
232
+ private currentQuantity: number,
233
+ ) {
234
+ super(id);
235
+ }
236
+
237
+ static create(id: OrderLineId, productId: ProductId): OrderLine {
238
+ return new OrderLine(id, productId, 1);
239
+ }
240
+
241
+ static fromSnapshot(snapshot: OrderLineSnapshot): OrderLine { // [!code ++]
242
+ return new OrderLine( // [!code ++]
243
+ new OrderLineId(snapshot.id), // [!code ++]
244
+ new ProductId(snapshot.productId), // [!code ++]
245
+ snapshot.quantity, // [!code ++]
246
+ ); // [!code ++]
247
+ } // [!code ++]
248
+
249
+ toSnapshot(): OrderLineSnapshot { // [!code ++]
250
+ return { // [!code ++]
251
+ id: this.id.value, // [!code ++]
252
+ productId: this.productId.value, // [!code ++]
253
+ quantity: this.currentQuantity, // [!code ++]
254
+ }; // [!code ++]
255
+ } // [!code ++]
256
+ }
257
+ ```
258
+
259
+ ### 4. Change it through a method
260
+
261
+ So that no caller can skip a rule, the quantity changes through a method named after what the
262
+ business does. A wrong quantity is returned in a `Result`, and nothing changes.
263
+
264
+ ```ts [src/ordering/domain/entities/order-line.entity.ts]
265
+ import { Entity } from "@alveolus/core"; // [!code --]
266
+ import { Entity, err, ok, type Result } from "@alveolus/core"; // [!code ++]
267
+
268
+ import { InvalidQuantity } from "../errors/invalid-quantity.error"; // [!code ++]
269
+ import { OrderLineId } from "../value-objects/order-line-id.identifier";
270
+ import { ProductId } from "../value-objects/product-id.identifier";
271
+
272
+ export type OrderLineSnapshot = {
273
+ readonly id: string;
274
+ readonly productId: string;
275
+ readonly quantity: number;
276
+ };
277
+
278
+ export class OrderLine extends Entity<OrderLineId, OrderLineSnapshot> {
279
+ private constructor(
280
+ id: OrderLineId,
281
+ private readonly productId: ProductId,
282
+ private currentQuantity: number,
283
+ ) {
284
+ super(id);
285
+ }
286
+
287
+ static create(id: OrderLineId, productId: ProductId): OrderLine {
288
+ return new OrderLine(id, productId, 1);
289
+ }
290
+
291
+ static fromSnapshot(snapshot: OrderLineSnapshot): OrderLine {
292
+ return new OrderLine(
293
+ new OrderLineId(snapshot.id),
294
+ new ProductId(snapshot.productId),
295
+ snapshot.quantity,
296
+ );
297
+ }
298
+
299
+ changeQuantity(quantity: number): Result<void, InvalidQuantity> { // [!code ++]
300
+ if (quantity <= 0) { // [!code ++]
301
+ return err(new InvalidQuantity({ quantity })); // [!code ++]
302
+ } // [!code ++]
303
+ this.currentQuantity = quantity; // [!code ++]
304
+ return ok(); // [!code ++]
305
+ } // [!code ++]
306
+
307
+ toSnapshot(): OrderLineSnapshot {
308
+ return {
309
+ id: this.id.value,
310
+ productId: this.productId.value,
311
+ quantity: this.currentQuantity,
312
+ };
313
+ }
314
+ }
315
+ ```
316
+
317
+ ### 5. Expose reads as getters
318
+
319
+ Callers need to read the quantity without changing it. Reads are getters: a public method must
320
+ return a `Result`, a getter does not.
321
+
322
+ ```ts [src/ordering/domain/entities/order-line.entity.ts]
323
+ import { Entity, err, ok, type Result } from "@alveolus/core";
324
+
325
+ import { InvalidQuantity } from "../errors/invalid-quantity.error";
326
+ import { OrderLineId } from "../value-objects/order-line-id.identifier";
327
+ import { ProductId } from "../value-objects/product-id.identifier";
328
+
329
+ export type OrderLineSnapshot = {
330
+ readonly id: string;
331
+ readonly productId: string;
332
+ readonly quantity: number;
333
+ };
334
+
335
+ export class OrderLine extends Entity<OrderLineId, OrderLineSnapshot> {
336
+ private constructor(
337
+ id: OrderLineId,
338
+ private readonly productId: ProductId,
339
+ private currentQuantity: number,
340
+ ) {
341
+ super(id);
342
+ }
343
+
344
+ static create(id: OrderLineId, productId: ProductId): OrderLine {
345
+ return new OrderLine(id, productId, 1);
346
+ }
347
+
348
+ static fromSnapshot(snapshot: OrderLineSnapshot): OrderLine {
349
+ return new OrderLine(
350
+ new OrderLineId(snapshot.id),
351
+ new ProductId(snapshot.productId),
352
+ snapshot.quantity,
353
+ );
354
+ }
355
+
356
+ get quantity(): number { // [!code ++]
357
+ return this.currentQuantity; // [!code ++]
358
+ } // [!code ++]
359
+
360
+ changeQuantity(quantity: number): Result<void, InvalidQuantity> {
361
+ if (quantity <= 0) {
362
+ return err(new InvalidQuantity({ quantity }));
363
+ }
364
+ this.currentQuantity = quantity;
365
+ return ok();
366
+ }
367
+
368
+ toSnapshot(): OrderLineSnapshot {
369
+ return {
370
+ id: this.id.value,
371
+ productId: this.productId.value,
372
+ quantity: this.currentQuantity,
373
+ };
374
+ }
375
+ }
376
+ ```
377
+
378
+ ### 6. Use it from its aggregate
379
+
380
+ Code outside the aggregate never holds a line: the [`Order`](./aggregates.md) creates it, changes
381
+ it, and saves it inside its own snapshot with `line.toSnapshot()`.
382
+
383
+ ```ts [src/ordering/domain/aggregates/order.aggregate.ts]
384
+ addLine(
385
+ lineId: OrderLineId,
386
+ productId: ProductId,
387
+ ): Result<void, OrderAlreadyPlaced> {
388
+ if (this.isPlaced) {
389
+ return err(new OrderAlreadyPlaced());
390
+ }
391
+ this.lines.push(OrderLine.create(lineId, productId));
392
+ return ok();
393
+ }
394
+ ```
395
+
396
+ ### 7. Check it
397
+
398
+ Run the checks. Three rules keep the entity the way it is now:
399
+
400
+ ```sh
401
+ npx alveolus arch check
402
+ ```
403
+
404
+ <div class="al-cards">
405
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-thrown-failure"><code>no-thrown-failure</code></a></span>Its public methods return a <code>Result</code>; reads are getters.</div>
406
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-aggregate-reference"><code>no-aggregate-reference</code></a></span>It keeps a <code>ProductId</code>, never a <code>Product</code>.</div>
407
+ <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>domain/entities/*.entity.ts</code>.</div>
408
+ </div>
409
+
410
+ A setter added later is reported:
411
+
412
+ ```
413
+ src/ordering/domain/entities/order-line.entity.ts
414
+ 38 error tactical/no-thrown-failure: OrderLine.setQuantity must return
415
+ a Result: expose reads as getters and return business failures
416
+ as values.
417
+ ```
418
+
419
+ ## Troubleshooting
420
+
421
+ **`Type 'OrderLineSnapshot' does not satisfy the constraint 'AnySnapshot'`**: the snapshot is an
422
+ `interface`, or a field holds an identifier or a value object. Declare it with `type` and write
423
+ raw values (`productId: string`).
424
+
425
+ ## See also
426
+
427
+ - [Aggregates](./aggregates.md), the root that owns entities
428
+ - [Value objects](./value-objects.md), for identifiers and things without identity
429
+ - [Domain errors](./domain-errors.md), what its methods return
430
+ - Rules: [`tactical/no-thrown-failure`](../../rules/tactical/no-thrown-failure.md), [`tactical/no-aggregate-reference`](../../rules/tactical/no-aggregate-reference.md), [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md)
431
+ - Vaughn Vernon, *Implementing Domain-Driven Design*, chapter 5, "Entities"