@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,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The domain layer in Domain-Driven Design with TypeScript: the model of the business, its objects and rules, free of frameworks and infrastructure."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Domain
|
|
6
|
+
|
|
7
|
+
The domain is the model of the business: its objects, its rules and what happens to them. It lives
|
|
8
|
+
in `domain/` and imports nothing but the domain and `@alveolus/core`.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
When the rule "an order cannot be placed empty" sits in a controller, a SQL query and a cron job,
|
|
13
|
+
each copy drifts and nobody knows which one is right. When it imports an ORM or a framework, it
|
|
14
|
+
cannot be read or tested without them.
|
|
15
|
+
|
|
16
|
+
::: tip The fix
|
|
17
|
+
The rules live in one place, in plain TypeScript classes named after the business. Everything else
|
|
18
|
+
calls them.
|
|
19
|
+
:::
|
|
20
|
+
|
|
21
|
+
## How the blocks fit together
|
|
22
|
+
|
|
23
|
+
<div class="al-diagram">
|
|
24
|
+
<svg viewBox="0 0 680 300" role="img" aria-label="The Order aggregate holds the Order root, its OrderLine entities and value objects such as OrderId. The root records the OrderPlaced domain event and returns domain errors such as EmptyOrder; a domain service such as OrderLimit checks rules across aggregates. Below, the Orders repository loads and saves the aggregate, the Clock port gives the time and the OrderSummary view is what queries read.">
|
|
25
|
+
<defs>
|
|
26
|
+
<marker id="domain-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="380" height="200" rx="14" />
|
|
31
|
+
<text class="note" x="24" y="30">Order · aggregate</text>
|
|
32
|
+
<rect class="box" x="118" y="44" width="160" height="48" rx="8" />
|
|
33
|
+
<text class="label" x="198" y="64" text-anchor="middle">Order</text>
|
|
34
|
+
<text class="note" x="198" y="82" text-anchor="middle">root</text>
|
|
35
|
+
<rect class="box" x="28" y="138" width="160" height="48" rx="8" />
|
|
36
|
+
<text class="label" x="108" y="158" text-anchor="middle">OrderLine</text>
|
|
37
|
+
<text class="note" x="108" y="176" text-anchor="middle">entity</text>
|
|
38
|
+
<rect class="box" x="208" y="138" width="160" height="48" rx="8" />
|
|
39
|
+
<text class="label" x="288" y="158" text-anchor="middle">OrderId</text>
|
|
40
|
+
<text class="note" x="288" y="176" text-anchor="middle">value object</text>
|
|
41
|
+
<path class="link" d="M 170 92 L 120 136" marker-end="url(#domain-overview-arrow)" />
|
|
42
|
+
<path class="link" d="M 226 92 L 276 136" marker-end="url(#domain-overview-arrow)" />
|
|
43
|
+
<rect class="box" x="472" y="20" width="200" height="48" rx="8" />
|
|
44
|
+
<text class="label" x="572" y="40" text-anchor="middle">OrderPlaced</text>
|
|
45
|
+
<text class="note" x="572" y="58" text-anchor="middle">domain event · recorded</text>
|
|
46
|
+
<rect class="box" x="472" y="88" width="200" height="48" rx="8" />
|
|
47
|
+
<text class="label" x="572" y="108" text-anchor="middle">EmptyOrder</text>
|
|
48
|
+
<text class="note" x="572" y="126" text-anchor="middle">domain error · returned</text>
|
|
49
|
+
<rect class="box" x="472" y="156" width="200" height="48" rx="8" />
|
|
50
|
+
<text class="label" x="572" y="176" text-anchor="middle">OrderLimit</text>
|
|
51
|
+
<text class="note" x="572" y="194" text-anchor="middle">domain service</text>
|
|
52
|
+
<path class="link" d="M 278 62 L 470 44" marker-end="url(#domain-overview-arrow)" />
|
|
53
|
+
<path class="link" d="M 278 72 L 470 112" marker-end="url(#domain-overview-arrow)" />
|
|
54
|
+
<path class="link" d="M 470 180 L 390 180" stroke-dasharray="4 4" marker-end="url(#domain-overview-arrow)" />
|
|
55
|
+
<text class="note" x="430" y="172" text-anchor="middle">checks</text>
|
|
56
|
+
<rect class="box" x="8" y="240" width="210" height="48" rx="8" />
|
|
57
|
+
<text class="label" x="113" y="260" text-anchor="middle">Orders</text>
|
|
58
|
+
<text class="note" x="113" y="278" text-anchor="middle">repository · load, save</text>
|
|
59
|
+
<path class="link" d="M 113 240 L 113 210" marker-end="url(#domain-overview-arrow)" />
|
|
60
|
+
<rect class="box" x="235" y="240" width="210" height="48" rx="8" />
|
|
61
|
+
<text class="label" x="340" y="260" text-anchor="middle">Clock</text>
|
|
62
|
+
<text class="note" x="340" y="278" text-anchor="middle">port · outside world</text>
|
|
63
|
+
<rect class="box" x="462" y="240" width="210" height="48" rx="8" />
|
|
64
|
+
<text class="label" x="567" y="260" text-anchor="middle">OrderSummary</text>
|
|
65
|
+
<text class="note" x="567" y="278" text-anchor="middle">view · read by queries</text>
|
|
66
|
+
</svg>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="al-cards">
|
|
70
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Model</span>An <a href="/core/domain/aggregates">aggregate</a> groups <a href="/core/domain/entities">entities</a> and <a href="/core/domain/value-objects">value objects</a> behind a root that keeps the rules.</div>
|
|
71
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Outcomes</span>A change records a <a href="/core/domain/domain-events">domain event</a>; a refusal returns a <a href="/core/domain/domain-errors">domain error</a>.</div>
|
|
72
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Outside world</span><a href="/core/domain/repositories">Repositories</a> and <a href="/core/domain/ports">ports</a> say what the domain needs, in its words. Adapters do the rest.</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
## The building blocks
|
|
76
|
+
|
|
77
|
+
| Building block | What it is | Use it when |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| [Aggregates](./aggregates.md) | A group of objects changed together through one root, which keeps their rules. | Some rules must hold after every change. |
|
|
80
|
+
| [Entities](./entities.md) | An object defined by its identity, inside an aggregate. | A part of an aggregate changes over time and must be told apart. |
|
|
81
|
+
| [Value objects](./value-objects.md) | An immutable value compared by its attributes, and the typed identifiers. | A number or a string has rules or a unit: an amount, an email, an id. |
|
|
82
|
+
| [Domain events](./domain-events.md) | Something that happened in the domain, in the past tense. | Something else must react to a change. |
|
|
83
|
+
| [Domain errors](./domain-errors.md) | An expected business failure, returned as a value. | A business rule refuses a request. |
|
|
84
|
+
| [Domain services](./domain-services.md) | A stateless operation that belongs to no single object. | A rule needs several aggregates and belongs to none. |
|
|
85
|
+
| [Ports](./ports.md) | What the domain needs from the outside world, in its own words. | The domain needs the time, an id, a payment, another context. |
|
|
86
|
+
| [Repositories](./repositories.md) | How aggregates are loaded and saved, and how views are read. | An aggregate must be stored, or a query must read data. |
|
|
87
|
+
| [Views](./views.md) | What a query returns. | A screen or an API needs data shaped for reading. |
|
|
88
|
+
|
|
89
|
+
## See also
|
|
90
|
+
|
|
91
|
+
- [Application](../application/index.md), the use cases that call the domain
|
|
92
|
+
- [Strategic](../strategic/index.md), how contexts meet
|
|
93
|
+
- Rules: [`layers/no-impure-domain`](../../rules/layers/no-impure-domain.md), [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md)
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Ports in hexagonal architecture with TypeScript: abstract classes through which the domain states what it needs from the outside world, in its own words."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Ports
|
|
6
|
+
|
|
7
|
+
A port is an abstract class through which the domain says, in its own words, what it needs from
|
|
8
|
+
the outside world.
|
|
9
|
+
|
|
10
|
+
<dl class="al-glance">
|
|
11
|
+
<dt>Layer</dt><dd>Domain, implemented by a driven adapter</dd>
|
|
12
|
+
<dt>File</dt><dd><code>domain/ports/payments.port.ts</code></dd>
|
|
13
|
+
<dt>Extends</dt><dd><a href="#api"><code>Port</code></a></dd>
|
|
14
|
+
<dt>Used by</dt><dd><a href="/core/application/command-handlers">Command handlers</a>, <a href="/core/application/query-handlers">query 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/layers/no-portless-adapter"><code>layers/no-portless-adapter</code></a>, <a href="/rules/layers/no-impure-domain"><code>layers/no-impure-domain</code></a></dd>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
Placing an order records an event with an id and a date. If the domain calls `randomUUID()` and
|
|
21
|
+
`new Date()` itself, no test can predict the event. The same goes for a payment: if the use case
|
|
22
|
+
calls the Stripe SDK, it speaks Stripe, and changing provider means rewriting it.
|
|
23
|
+
|
|
24
|
+
::: tip The fix
|
|
25
|
+
The domain declares a port for each need: `Clock`, `IdGenerator`, `Payments`. The handler receives
|
|
26
|
+
the port; a driven adapter implements it with a technology. Tests pass a fixed clock, production
|
|
27
|
+
passes the system clock, and the use case never knows which.
|
|
28
|
+
:::
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
A port is the contract; an adapter is one way to fulfil it. The domain owns the contract, so it
|
|
33
|
+
never depends on a technology: the dependency points inwards.
|
|
34
|
+
|
|
35
|
+
<div class="al-cards">
|
|
36
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Declare</span>The domain extends <code>Port</code> with abstract methods, named after what it needs.</div>
|
|
37
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Implement</span>A driven adapter extends the port and does the work with a library, a database or an API.</div>
|
|
38
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Wire</span>The composition root passes the adapter where the port is expected.</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export abstract class Payments extends Port {
|
|
43
|
+
abstract charge(
|
|
44
|
+
orderId: OrderId,
|
|
45
|
+
amount: Money,
|
|
46
|
+
): Promise<Result<void, PaymentDeclined>>;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`@alveolus/core` ships two ports every project needs, `Clock` and `IdGenerator`, and builds its
|
|
51
|
+
other contracts on `Port`: [repositories](./repositories.md), the
|
|
52
|
+
[`Outbox`](../application/outbox.md), the [`UnitOfWork`](../application/unit-of-work.md) and the
|
|
53
|
+
[`EventPublisher`](../application/event-publishers.md).
|
|
54
|
+
|
|
55
|
+
## Where it fits
|
|
56
|
+
|
|
57
|
+
The command handler receives ports in its constructor and calls them. Each port is implemented by
|
|
58
|
+
an adapter in `driven/`.
|
|
59
|
+
|
|
60
|
+
<div class="al-diagram">
|
|
61
|
+
<svg viewBox="0 0 680 250" role="img" aria-label="The PlaceOrderHandler calls the Clock, IdGenerator and Orders ports, declared in the domain. The SystemClock, RandomIdGenerator and PgOrders adapters, in driven, extend them.">
|
|
62
|
+
<defs>
|
|
63
|
+
<marker id="ports-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
64
|
+
<path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
|
|
65
|
+
</marker>
|
|
66
|
+
</defs>
|
|
67
|
+
<text class="note" x="93" y="24" text-anchor="middle">application</text>
|
|
68
|
+
<text class="note" x="335" y="24" text-anchor="middle">domain: ports</text>
|
|
69
|
+
<text class="note" x="586" y="24" text-anchor="middle">driven: adapters</text>
|
|
70
|
+
<rect class="box" x="8" y="112" width="170" height="56" rx="8" />
|
|
71
|
+
<text class="label" x="93" y="136" text-anchor="middle">PlaceOrderHandler</text>
|
|
72
|
+
<text class="note" x="93" y="156" text-anchor="middle">command handler</text>
|
|
73
|
+
<rect class="boundary" x="250" y="44" width="170" height="48" rx="8" />
|
|
74
|
+
<text class="label" x="335" y="64" text-anchor="middle">Clock</text>
|
|
75
|
+
<text class="note" x="335" y="82" text-anchor="middle">port</text>
|
|
76
|
+
<rect class="boundary" x="250" y="116" width="170" height="48" rx="8" />
|
|
77
|
+
<text class="label" x="335" y="136" text-anchor="middle">IdGenerator</text>
|
|
78
|
+
<text class="note" x="335" y="154" text-anchor="middle">port</text>
|
|
79
|
+
<rect class="boundary" x="250" y="188" width="170" height="48" rx="8" />
|
|
80
|
+
<text class="label" x="335" y="208" text-anchor="middle">Orders</text>
|
|
81
|
+
<text class="note" x="335" y="226" text-anchor="middle">repository, a port</text>
|
|
82
|
+
<rect class="box" x="500" y="44" width="172" height="48" rx="8" />
|
|
83
|
+
<text class="label" x="586" y="64" text-anchor="middle">SystemClock</text>
|
|
84
|
+
<text class="note" x="586" y="82" text-anchor="middle">adapter</text>
|
|
85
|
+
<rect class="box" x="500" y="116" width="172" height="48" rx="8" />
|
|
86
|
+
<text class="label" x="586" y="136" text-anchor="middle">RandomIdGenerator</text>
|
|
87
|
+
<text class="note" x="586" y="154" text-anchor="middle">adapter</text>
|
|
88
|
+
<rect class="box" x="500" y="188" width="172" height="48" rx="8" />
|
|
89
|
+
<text class="label" x="586" y="208" text-anchor="middle">PgOrders</text>
|
|
90
|
+
<text class="note" x="586" y="226" text-anchor="middle">adapter</text>
|
|
91
|
+
<path class="link" d="M 178 140 L 248 68" marker-end="url(#ports-arrow)" />
|
|
92
|
+
<path class="link" d="M 178 140 L 248 140" marker-end="url(#ports-arrow)" />
|
|
93
|
+
<path class="link" d="M 178 140 L 248 212" marker-end="url(#ports-arrow)" />
|
|
94
|
+
<path class="link" d="M 498 68 L 422 68" stroke-dasharray="4 4" marker-end="url(#ports-arrow)" />
|
|
95
|
+
<path class="link" d="M 498 140 L 422 140" stroke-dasharray="4 4" marker-end="url(#ports-arrow)" />
|
|
96
|
+
<path class="link" d="M 498 212 L 422 212" stroke-dasharray="4 4" marker-end="url(#ports-arrow)" />
|
|
97
|
+
<text class="note" x="460" y="60" text-anchor="middle">extends</text>
|
|
98
|
+
</svg>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
::: tip
|
|
102
|
+
Arrows point towards the domain: the handler calls the port, the adapter extends it. Nothing in the
|
|
103
|
+
domain or the application imports an adapter.
|
|
104
|
+
:::
|
|
105
|
+
|
|
106
|
+
## API
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { Clock, IdGenerator, Port } from "@alveolus/core";
|
|
110
|
+
// or: from "@alveolus/core/ports"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Port
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
abstract class Port {}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`Port` has no members: extending it marks the class as a port. Declare yours abstract, in
|
|
120
|
+
`domain/ports/`, with the methods the use cases need; its adapter, in
|
|
121
|
+
`driven/<technology>/adapters/`, implements every one. Everything a driven adapter implements is
|
|
122
|
+
a port: your own ports, the [repositories](./repositories.md), and the
|
|
123
|
+
[`EventPublisher`](../application/event-publishers.md), [`Outbox`](../application/outbox.md) and
|
|
124
|
+
[`UnitOfWork`](../application/unit-of-work.md) of core.
|
|
125
|
+
|
|
126
|
+
### `Clock.now()` <Badge type="info" text="abstract" /> <Badge type="tip" text="called by the command handler" />
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
abstract now(): Date
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The current date. `Clock` extends `Port`.
|
|
133
|
+
|
|
134
|
+
### `IdGenerator.next()` <Badge type="info" text="abstract" /> <Badge type="tip" text="called by the command handler" />
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
abstract next(): string
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A new unique id. Wrap it in your identifier: `new OrderId(ids.next())`. `IdGenerator` extends
|
|
141
|
+
`Port`.
|
|
142
|
+
|
|
143
|
+
::: warning Caveats
|
|
144
|
+
- Ports are declared in `domain/ports/*.port.ts`, of a bounded context or of the shared kernel;
|
|
145
|
+
`alveolus arch check` reports a port declared anywhere else.
|
|
146
|
+
- Core ships no implementation of `Clock` or `IdGenerator`: they are adapters of your project.
|
|
147
|
+
:::
|
|
148
|
+
|
|
149
|
+
## Usage
|
|
150
|
+
|
|
151
|
+
Build `Payments`, what the ordering domain needs to charge an order, then implement it with
|
|
152
|
+
Stripe. Each step shows the whole file it changes: added lines are highlighted, replaced lines are struck out.
|
|
153
|
+
|
|
154
|
+
<div class="al-cards">
|
|
155
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-say-what-the-domain-needs">Say what the domain needs</a></span>In its own words.</div>
|
|
156
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-the-port">Declare the port</a></span>An abstract class in the domain.</div>
|
|
157
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-describe-the-operation">Describe the operation</a></span>Domain types in, a Result out.</div>
|
|
158
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-implement-it-in-a-driven-adapter">Implement it in a driven adapter</a></span>The provider stays at the edge.</div>
|
|
159
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-use-it-from-a-handler">Use it from a handler</a></span>Through the abstraction.</div>
|
|
160
|
+
<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>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
### 1. Say what the domain needs
|
|
164
|
+
|
|
165
|
+
Placing an order charges the customer. The domain says it with its own types: an
|
|
166
|
+
[`OrderId`](./value-objects.md#identifier), an amount in [`Money`](./value-objects.md), and a
|
|
167
|
+
`PaymentDeclined` [domain error](./domain-errors.md).
|
|
168
|
+
|
|
169
|
+
### 2. Declare the port
|
|
170
|
+
|
|
171
|
+
So that the domain depends on what it needs, not on a provider, the need is an abstract class
|
|
172
|
+
named in the words of the domain. It is also the token the composition root binds an adapter to.
|
|
173
|
+
|
|
174
|
+
```ts [src/ordering/domain/ports/payments.port.ts]
|
|
175
|
+
import { Port } from "@alveolus/core";
|
|
176
|
+
|
|
177
|
+
export abstract class Payments extends Port {}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 3. Describe the operation
|
|
181
|
+
|
|
182
|
+
Each operation takes and returns domain objects. A refusal the domain must handle is a domain
|
|
183
|
+
error in a `Result`; a broken connection stays an exception.
|
|
184
|
+
|
|
185
|
+
```ts [src/ordering/domain/ports/payments.port.ts]
|
|
186
|
+
import { Port } from "@alveolus/core"; // [!code --]
|
|
187
|
+
import { Port, type Result } from "@alveolus/core"; // [!code ++]
|
|
188
|
+
|
|
189
|
+
import type { PaymentDeclined } from "../errors/payment-declined.error"; // [!code ++]
|
|
190
|
+
import type { Money } from "../value-objects/money.value-object"; // [!code ++]
|
|
191
|
+
import type { OrderId } from "../value-objects/order-id.identifier"; // [!code ++]
|
|
192
|
+
|
|
193
|
+
export abstract class Payments extends Port {} // [!code --]
|
|
194
|
+
export abstract class Payments extends Port { // [!code ++]
|
|
195
|
+
abstract charge( // [!code ++]
|
|
196
|
+
orderId: OrderId, // [!code ++]
|
|
197
|
+
amount: Money, // [!code ++]
|
|
198
|
+
): Promise<Result<void, PaymentDeclined>>; // [!code ++]
|
|
199
|
+
} // [!code ++]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 4. Implement it in a driven adapter
|
|
203
|
+
|
|
204
|
+
Stripe stays at the edge: the adapter extends the port, translates the call, and turns a declined
|
|
205
|
+
payment into the error of the domain.
|
|
206
|
+
|
|
207
|
+
```ts [src/ordering/driven/stripe/adapters/stripe-payments.adapter.ts]
|
|
208
|
+
import { err, ok, type Result } from "@alveolus/core";
|
|
209
|
+
import type Stripe from "stripe";
|
|
210
|
+
|
|
211
|
+
import {
|
|
212
|
+
PaymentDeclined,
|
|
213
|
+
} from "../../../domain/errors/payment-declined.error";
|
|
214
|
+
import { Payments } from "../../../domain/ports/payments.port";
|
|
215
|
+
import type {
|
|
216
|
+
Money,
|
|
217
|
+
} from "../../../domain/value-objects/money.value-object";
|
|
218
|
+
import type {
|
|
219
|
+
OrderId,
|
|
220
|
+
} from "../../../domain/value-objects/order-id.identifier";
|
|
221
|
+
|
|
222
|
+
export class StripePayments extends Payments {
|
|
223
|
+
constructor(private readonly stripe: Stripe) {
|
|
224
|
+
super();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async charge(
|
|
228
|
+
orderId: OrderId,
|
|
229
|
+
amount: Money,
|
|
230
|
+
): Promise<Result<void, PaymentDeclined>> {
|
|
231
|
+
const intent = await this.stripe.paymentIntents.create({
|
|
232
|
+
amount: Math.round(amount.amount * 100),
|
|
233
|
+
currency: amount.currency,
|
|
234
|
+
metadata: { orderId: orderId.value },
|
|
235
|
+
});
|
|
236
|
+
if (intent.status !== "succeeded") {
|
|
237
|
+
return err(new PaymentDeclined({ orderId: orderId.value }));
|
|
238
|
+
}
|
|
239
|
+
return ok();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 5. Use it from a handler
|
|
245
|
+
|
|
246
|
+
The handler asks for `Payments` and never knows which provider answers; the composition root
|
|
247
|
+
passes a `StripePayments`.
|
|
248
|
+
|
|
249
|
+
```ts [src/ordering/application/commands/pay-order.command.ts]
|
|
250
|
+
const charged = await this.payments.charge(order.id, total);
|
|
251
|
+
if (!charged.ok) {
|
|
252
|
+
return charged;
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 6. Check it
|
|
257
|
+
|
|
258
|
+
Run the checks. Three rules keep the port and its adapter the way they are now:
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
npx alveolus arch check
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
<div class="al-cards">
|
|
265
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-portless-adapter"><code>no-portless-adapter</code></a></span>The adapter extends the port it implements, declared in the domain.</div>
|
|
266
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-impure-domain"><code>no-impure-domain</code></a></span>The port imports the domain only: Stripe never reaches it.</div>
|
|
267
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-misplaced-class"><code>no-misplaced-class</code></a></span><code>domain/ports/*.port.ts</code> and <code>driven/<technology>/adapters/*.adapter.ts</code>.</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
An adapter that forgets its port is reported:
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
src/ordering/driven/stripe/adapters/stripe-payments.adapter.ts
|
|
274
|
+
16 error layers/no-portless-adapter: StripePayments is a driven adapter
|
|
275
|
+
but extends no Port: extend the port it implements.
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## See also
|
|
279
|
+
|
|
280
|
+
- [Repositories](./repositories.md), the ports for persistence
|
|
281
|
+
- [Anti-corruption layers](../strategic/anti-corruption-layers.md), ports that read another context
|
|
282
|
+
- [Project layout](../../guide/project-layout.md), where ports and adapters live
|
|
283
|
+
- Rules: [`layers/no-portless-adapter`](../../rules/layers/no-portless-adapter.md), [`layers/no-impure-domain`](../../rules/layers/no-impure-domain.md)
|
|
284
|
+
- Vaughn Vernon, *Implementing Domain-Driven Design*, chapter 4, "Architecture"
|