@alveolus/arch 0.1.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 +8 -1
  2. package/dist/bin.mjs +4 -2
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/{cli-CwPCGjDg.mjs → docs-DsQHpTtV.mjs} +287 -38
  5. package/dist/docs-DsQHpTtV.mjs.map +1 -0
  6. package/dist/index.d.mts +90 -36
  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-CwPCGjDg.mjs.map +0 -1
@@ -0,0 +1,416 @@
1
+ ---
2
+ description: "The transactional outbox pattern in TypeScript: store integration events in the same transaction as the change, then relay them so none is lost."
3
+ ---
4
+
5
+ # Outbox
6
+
7
+ An outbox stores the integration events of a change in the same transaction as the change, then a
8
+ relay publishes them, so none is lost.
9
+
10
+ <dl class="al-glance">
11
+ <dt>Layer</dt><dd>Application (a port, and the <code>OutboxRelay</code> class)</dd>
12
+ <dt>File</dt><dd><code>shared-kernel/driven/pg/adapters/pg-outbox.adapter.ts</code> (your adapter)</dd>
13
+ <dt>Extends</dt><dd><a href="#api"><code>Outbox</code></a></dd>
14
+ <dt>Called by</dt><dd><a href="/core/application/command-handlers">Command handlers</a> (<code>add</code>), <code>OutboxRelay</code> (<code>pending</code>, <code>markPublished</code>)</dd>
15
+ <dt>Checked by</dt><dd><a href="/rules/layers/no-portless-adapter"><code>layers/no-portless-adapter</code></a>, <a href="/rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a></dd>
16
+ </dl>
17
+
18
+ ## Why
19
+
20
+ When an order is placed, shipping and billing must hear about it. If the handler saves the order
21
+ then publishes `OrderPlaced` to the broker, two things go wrong. The process stops between the two:
22
+ the order is placed and nobody hears about it. The broker is down: the command fails although the
23
+ order is valid.
24
+
25
+ ::: tip The fix
26
+ The handler adds the events to the outbox, in the same [unit of work](./unit-of-work.md) as the
27
+ order: both are saved, or neither. Later, the `OutboxRelay` reads the pending events and hands them
28
+ to the [event publisher](./event-publishers.md), retrying until it succeeds.
29
+ :::
30
+
31
+ ## How it works
32
+
33
+ The outbox splits publishing in two moments: storing, in the transaction of the change, and
34
+ relaying, in the background.
35
+
36
+ <div class="al-diagram">
37
+ <svg viewBox="0 0 700 250" role="img" aria-label="Inside one unit of work, the command handler saves the aggregate and adds its integration events to the outbox. Later, the outbox relay reads pending events, calls the event publisher, which sends them to other contexts, then marks them as published.">
38
+ <defs>
39
+ <marker id="outbox-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
40
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
41
+ </marker>
42
+ </defs>
43
+ <rect class="boundary" x="8" y="8" width="300" height="234" rx="14" />
44
+ <text class="note" x="24" y="32">one unit of work</text>
45
+ <rect class="box" x="28" y="48" width="260" height="48" rx="8" />
46
+ <text class="label" x="158" y="70" text-anchor="middle">PlaceOrderHandler</text>
47
+ <text class="note" x="158" y="87" text-anchor="middle">places the order</text>
48
+ <rect class="box" x="28" y="150" width="120" height="70" rx="8" />
49
+ <text class="label" x="88" y="180" text-anchor="middle">orders</text>
50
+ <text class="note" x="88" y="200" text-anchor="middle">save(order)</text>
51
+ <rect class="box" x="168" y="150" width="120" height="70" rx="8" />
52
+ <text class="label" x="228" y="180" text-anchor="middle">outbox</text>
53
+ <text class="note" x="228" y="200" text-anchor="middle">add(events)</text>
54
+ <path class="link" d="M 88 96 L 88 148" marker-end="url(#outbox-arrow)" />
55
+ <path class="link" d="M 228 96 L 228 148" marker-end="url(#outbox-arrow)" />
56
+ <rect class="box" x="372" y="150" width="140" height="70" rx="8" />
57
+ <text class="label" x="442" y="180" text-anchor="middle">OutboxRelay</text>
58
+ <text class="note" x="442" y="200" text-anchor="middle">relay()</text>
59
+ <path class="link" d="M 370 185 L 290 185" marker-end="url(#outbox-arrow)" />
60
+ <text class="note" x="340" y="176" text-anchor="middle">pending</text>
61
+ <rect class="box" x="372" y="48" width="140" height="48" rx="8" />
62
+ <text class="label" x="442" y="70" text-anchor="middle">EventPublisher</text>
63
+ <text class="note" x="442" y="87" text-anchor="middle">publish(events)</text>
64
+ <path class="link" d="M 442 150 L 442 98" marker-end="url(#outbox-arrow)" />
65
+ <rect class="box" x="546" y="48" width="146" height="48" rx="8" />
66
+ <text class="label" x="619" y="70" text-anchor="middle">other contexts</text>
67
+ <text class="note" x="619" y="87" text-anchor="middle">broker, consumers</text>
68
+ <path class="link" d="M 512 72 L 544 72" marker-end="url(#outbox-arrow)" />
69
+ </svg>
70
+ </div>
71
+
72
+ Each call to `relay()` does three things:
73
+
74
+ <div class="al-cards">
75
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Read a batch</span><code>outbox.pending(n)</code> returns up to <code>n</code> unpublished events, oldest first.</div>
76
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Publish it</span><code>publisher.publish(events)</code> sends them. If it throws, the events stay pending for the next call.</div>
77
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Mark it</span><code>outbox.markPublished(ids)</code> records that they left. The next call skips them.</div>
78
+ </div>
79
+
80
+ ## Where it fits
81
+
82
+ In the PlaceOrder flow, the outbox is the last step of the command handler, inside its unit of work.
83
+
84
+ <div class="al-diagram">
85
+ <svg viewBox="0 0 680 300" role="img" aria-label="A request goes from a controller to 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.">
86
+ <defs>
87
+ <marker id="outbox-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
88
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
89
+ </marker>
90
+ </defs>
91
+ <rect class="box" x="8" y="122" width="130" height="56" rx="8" />
92
+ <text class="label" x="73" y="146" text-anchor="middle">Controller</text>
93
+ <text class="note" x="73" y="166" text-anchor="middle">driving adapter</text>
94
+ <path class="link" d="M 138 150 L 178 150" marker-end="url(#outbox-flow-arrow)" />
95
+ <rect class="box" x="180" y="122" width="180" height="56" rx="8" />
96
+ <text class="label" x="270" y="146" text-anchor="middle">PlaceOrderHandler</text>
97
+ <text class="note" x="270" y="166" text-anchor="middle">command handler</text>
98
+ <text class="note" x="270" y="204" text-anchor="middle">one unit of work</text>
99
+ <rect class="box" x="440" y="24" width="232" height="48" rx="8" />
100
+ <text class="label" x="556" y="44" text-anchor="middle">1 · orders.findById(id)</text>
101
+ <text class="note" x="556" y="62" text-anchor="middle">loads the Order</text>
102
+ <rect class="box" x="440" y="92" width="232" height="48" rx="8" />
103
+ <text class="label" x="556" y="112" text-anchor="middle">2 · order.place(…)</text>
104
+ <text class="note" x="556" y="130" text-anchor="middle">rules + event</text>
105
+ <rect class="box" x="440" y="160" width="232" height="48" rx="8" />
106
+ <text class="label" x="556" y="180" text-anchor="middle">3 · orders.save(order)</text>
107
+ <text class="note" x="556" y="198" text-anchor="middle">stores its snapshot</text>
108
+ <rect class="boundary" x="440" y="228" width="232" height="48" rx="8" />
109
+ <text class="label" x="556" y="248" text-anchor="middle">4 · outbox.add(events)</text>
110
+ <text class="note" x="556" y="266" text-anchor="middle">this page: stored, not sent</text>
111
+ <path class="link" d="M 360 150 L 438 48" marker-end="url(#outbox-flow-arrow)" />
112
+ <path class="link" d="M 360 150 L 438 116" marker-end="url(#outbox-flow-arrow)" />
113
+ <path class="link" d="M 360 150 L 438 184" marker-end="url(#outbox-flow-arrow)" />
114
+ <path class="link" d="M 360 150 L 438 252" marker-end="url(#outbox-flow-arrow)" />
115
+ </svg>
116
+ </div>
117
+
118
+ The domain events are first turned into integration events by an
119
+ [event translator](./event-translators.md): the outbox stores JSON, never domain classes.
120
+
121
+ ::: tip
122
+ A command handler never publishes. It stores; the relay publishes.
123
+ :::
124
+
125
+ ## API
126
+
127
+ ```ts
128
+ import { Outbox, OutboxRelay } from "@alveolus/core";
129
+ // or: import { Outbox, OutboxRelay } from "@alveolus/core/outbox";
130
+ ```
131
+
132
+ `Outbox` is the port you implement; `OutboxRelay` is a class provided by core that moves its
133
+ events to the [event publisher](./event-publishers.md).
134
+
135
+ ### `add(events)` <Badge type="info" text="abstract" /> <Badge type="tip" text="you implement it" /> <Badge type="tip" text="called by the command handler" />
136
+
137
+ ```ts
138
+ abstract add(events: readonly AnyIntegrationEvent[]): Promise<void>
139
+ ```
140
+
141
+ Stores the events of the change, in the current transaction.
142
+
143
+ ### `pending(limit)` <Badge type="info" text="abstract" /> <Badge type="tip" text="you implement it" /> <Badge type="tip" text="called by the OutboxRelay" />
144
+
145
+ ```ts
146
+ abstract pending(
147
+ limit: number,
148
+ ): Promise<readonly AnyIntegrationEvent[]>
149
+ ```
150
+
151
+ Returns up to `limit` unpublished events, oldest first.
152
+
153
+ ### `markPublished(ids)` <Badge type="info" text="abstract" /> <Badge type="tip" text="you implement it" /> <Badge type="tip" text="called by the OutboxRelay" />
154
+
155
+ ```ts
156
+ abstract markPublished(ids: readonly string[]): Promise<void>
157
+ ```
158
+
159
+ Marks the events with these ids as published.
160
+
161
+ ### `new OutboxRelay(outbox, publisher, batchSize?)` <Badge type="tip" text="called by the composition root" />
162
+
163
+ ```ts
164
+ constructor(
165
+ outbox: Outbox,
166
+ publisher: EventPublisher,
167
+ batchSize: number = 100,
168
+ )
169
+ ```
170
+
171
+ Builds the relay from the outbox to the publisher. `batchSize` is how many events one call to
172
+ `relay` reads.
173
+
174
+ ### `relay()` <Badge type="tip" text="called by a timer, a cron job or a worker" />
175
+
176
+ ```ts
177
+ relay(): Promise<number>
178
+ ```
179
+
180
+ Reads one batch from `pending`, publishes it in one call, marks it published, and returns how many
181
+ events it published: `0` when none is pending.
182
+
183
+ ::: warning Caveats
184
+ - Delivery is at least once: if the relay stops between publishing and marking, the events are
185
+ published again. Consumers ignore duplicates by `id`.
186
+ - Several relays running at once may publish the same batch. Lock the rows in `pending` (for
187
+ instance `FOR UPDATE SKIP LOCKED`) if you run more than one.
188
+ - `relay()` publishes one batch per call. Call it on a schedule, not once.
189
+ :::
190
+
191
+ ## Usage
192
+
193
+ Build an outbox in PostgreSQL. Each step shows the whole file: added lines are highlighted, replaced lines are struck out.
194
+
195
+ <div class="al-cards">
196
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-share-the-transaction">Share the transaction</a></span>Write where the order is saved.</div>
197
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-declare-the-adapter">Declare the adapter</a></span>Extend the port.</div>
198
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-add-inside-the-transaction">Add inside the transaction</a></span>Saved with the order, or not at all.</div>
199
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-read-what-is-pending">Read what is pending</a></span>Oldest first, a batch at a time.</div>
200
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-mark-them-published">Mark them published</a></span>Never sent twice on purpose.</div>
201
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">6</span><a href="#_6-run-the-relay">Run the relay</a></span>On a schedule, in a driving adapter.</div>
202
+ <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>
203
+ </div>
204
+
205
+ ### 1. Share the transaction
206
+
207
+ The outbox writes in the transaction of the [unit of work](./unit-of-work.md#usage), which shares its connection through an `AsyncLocalStorage`: build that first.
208
+
209
+ ### 2. Declare the adapter
210
+
211
+ The adapter extends `Outbox` and receives the pool, for the relay, and the storage of the current connection, for the handler.
212
+
213
+ ```ts [src/shared-kernel/driven/pg/adapters/pg-outbox.adapter.ts]
214
+ import type { AsyncLocalStorage } from "node:async_hooks";
215
+
216
+ import { type AnyIntegrationEvent, Outbox } from "@alveolus/core";
217
+ import type { Pool, PoolClient } from "pg";
218
+
219
+ export class PgOutbox extends Outbox {
220
+ constructor(
221
+ private readonly pool: Pool,
222
+ private readonly current: AsyncLocalStorage<PoolClient>,
223
+ ) {
224
+ super();
225
+ }
226
+ }
227
+ ```
228
+
229
+ TypeScript now asks for `add`, `pending` and `markPublished`: the next steps add them.
230
+
231
+ ### 3. Add inside the transaction
232
+
233
+ So that an event is never stored for an order that was not saved, `add` writes through the connection of the current unit of work, and refuses to run outside one. Each event is stored as it is, in a `jsonb` column.
234
+
235
+ ```ts [src/shared-kernel/driven/pg/adapters/pg-outbox.adapter.ts]
236
+ import type { AsyncLocalStorage } from "node:async_hooks";
237
+
238
+ import { type AnyIntegrationEvent, Outbox } from "@alveolus/core";
239
+ import type { Pool, PoolClient } from "pg";
240
+
241
+ export class PgOutbox extends Outbox {
242
+ constructor(
243
+ private readonly pool: Pool,
244
+ private readonly current: AsyncLocalStorage<PoolClient>,
245
+ ) {
246
+ super();
247
+ }
248
+
249
+ async add(events: readonly AnyIntegrationEvent[]): Promise<void> { // [!code ++]
250
+ const client = this.current.getStore(); // [!code ++]
251
+ if (client === undefined) { // [!code ++]
252
+ throw new Error("PgOutbox.add runs inside a unit of work."); // [!code ++]
253
+ } // [!code ++]
254
+ for (const event of events) { // [!code ++]
255
+ await client.query( // [!code ++]
256
+ "INSERT INTO outbox (id, event) VALUES ($1, $2)", // [!code ++]
257
+ [event.id, event], // [!code ++]
258
+ ); // [!code ++]
259
+ } // [!code ++]
260
+ } // [!code ++]
261
+ }
262
+ ```
263
+
264
+ ### 4. Read what is pending
265
+
266
+ The relay reads the events not yet published, in the order they were added, through the pool: it runs outside any transaction.
267
+
268
+ ```ts [src/shared-kernel/driven/pg/adapters/pg-outbox.adapter.ts]
269
+ import type { AsyncLocalStorage } from "node:async_hooks";
270
+
271
+ import { type AnyIntegrationEvent, Outbox } from "@alveolus/core";
272
+ import type { Pool, PoolClient } from "pg";
273
+
274
+ export class PgOutbox extends Outbox {
275
+ constructor(
276
+ private readonly pool: Pool,
277
+ private readonly current: AsyncLocalStorage<PoolClient>,
278
+ ) {
279
+ super();
280
+ }
281
+
282
+ async add(events: readonly AnyIntegrationEvent[]): Promise<void> {
283
+ const client = this.current.getStore();
284
+ if (client === undefined) {
285
+ throw new Error("PgOutbox.add runs inside a unit of work.");
286
+ }
287
+ for (const event of events) {
288
+ await client.query(
289
+ "INSERT INTO outbox (id, event) VALUES ($1, $2)",
290
+ [event.id, event],
291
+ );
292
+ }
293
+ }
294
+
295
+ async pending( // [!code ++]
296
+ limit: number, // [!code ++]
297
+ ): Promise<readonly AnyIntegrationEvent[]> { // [!code ++]
298
+ const { rows } = await this.pool.query( // [!code ++]
299
+ `SELECT event FROM outbox // [!code ++]
300
+ WHERE published_at IS NULL // [!code ++]
301
+ ORDER BY created_at // [!code ++]
302
+ LIMIT $1`, // [!code ++]
303
+ [limit], // [!code ++]
304
+ ); // [!code ++]
305
+ return rows.map((row) => row.event); // [!code ++]
306
+ } // [!code ++]
307
+ }
308
+ ```
309
+
310
+ ### 5. Mark them published
311
+
312
+ Once the publisher has sent a batch, the relay marks it published, so the next run skips it.
313
+
314
+ ```ts [src/shared-kernel/driven/pg/adapters/pg-outbox.adapter.ts]
315
+ import type { AsyncLocalStorage } from "node:async_hooks";
316
+
317
+ import { type AnyIntegrationEvent, Outbox } from "@alveolus/core";
318
+ import type { Pool, PoolClient } from "pg";
319
+
320
+ export class PgOutbox extends Outbox {
321
+ constructor(
322
+ private readonly pool: Pool,
323
+ private readonly current: AsyncLocalStorage<PoolClient>,
324
+ ) {
325
+ super();
326
+ }
327
+
328
+ async add(events: readonly AnyIntegrationEvent[]): Promise<void> {
329
+ const client = this.current.getStore();
330
+ if (client === undefined) {
331
+ throw new Error("PgOutbox.add runs inside a unit of work.");
332
+ }
333
+ for (const event of events) {
334
+ await client.query(
335
+ "INSERT INTO outbox (id, event) VALUES ($1, $2)",
336
+ [event.id, event],
337
+ );
338
+ }
339
+ }
340
+
341
+ async pending(
342
+ limit: number,
343
+ ): Promise<readonly AnyIntegrationEvent[]> {
344
+ const { rows } = await this.pool.query(
345
+ `SELECT event FROM outbox
346
+ WHERE published_at IS NULL
347
+ ORDER BY created_at
348
+ LIMIT $1`,
349
+ [limit],
350
+ );
351
+ return rows.map((row) => row.event);
352
+ }
353
+
354
+ async markPublished(ids: readonly string[]): Promise<void> { // [!code ++]
355
+ await this.pool.query( // [!code ++]
356
+ "UPDATE outbox SET published_at = now() WHERE id = ANY($1)", // [!code ++]
357
+ [ids], // [!code ++]
358
+ ); // [!code ++]
359
+ } // [!code ++]
360
+ }
361
+ ```
362
+
363
+ This is the complete outbox.
364
+
365
+ ### 6. Run the relay
366
+
367
+ The composition root builds the relay, `new OutboxRelay(outbox, publisher, 100)`, and a driving adapter runs it on a schedule. A failure is logged: the events stay pending for the next tick.
368
+
369
+ ```ts [src/ordering/driving/timer/jobs/outbox-relay.job.ts]
370
+ import type { OutboxRelay } from "@alveolus/core";
371
+
372
+ export class OutboxRelayJob {
373
+ constructor(private readonly relay: OutboxRelay) {}
374
+
375
+ start(): NodeJS.Timeout {
376
+ return setInterval(() => this.tick(), 1000);
377
+ }
378
+
379
+ private async tick(): Promise<void> {
380
+ try {
381
+ await this.relay.relay();
382
+ } catch (error) {
383
+ console.error("Relay failed; events stay pending.", error);
384
+ }
385
+ }
386
+ }
387
+ ```
388
+
389
+ ### 7. Check it
390
+
391
+ Run the checks. Two rules keep the outbox the way it is now:
392
+
393
+ ```sh
394
+ npx alveolus arch check
395
+ ```
396
+
397
+ <div class="al-cards">
398
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-portless-adapter"><code>no-portless-adapter</code></a></span>It extends the port it implements.</div>
399
+ <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>driven/pg/adapters/*.adapter.ts</code>.</div>
400
+ </div>
401
+
402
+ The same class without <code>extends Outbox</code> is reported:
403
+
404
+ ```
405
+ src/shared-kernel/driven/pg/adapters/pg-outbox.adapter.ts
406
+ 6 error layers/no-portless-adapter: PgOutbox is a driven adapter
407
+ but extends no Port: extend the port it implements.
408
+ ```
409
+
410
+ ## See also
411
+
412
+ - [Unit of Work](./unit-of-work.md), the transaction the outbox is written in
413
+ - [Event translators](./event-translators.md), which build what the outbox stores
414
+ - [Integration events](./integration-events.md), what it stores
415
+ - [Event publishers](./event-publishers.md), what the relay calls
416
+ - Rules: [`layers/no-portless-adapter`](../../rules/layers/no-portless-adapter.md)