@alveolus/arch 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/bin.mjs +4 -2
- package/dist/bin.mjs.map +1 -1
- package/dist/{cli-P5PwH9OE.mjs → docs-DcFgskuN.mjs} +214 -43
- package/dist/docs-DcFgskuN.mjs.map +1 -0
- package/dist/index.d.mts +53 -12
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/docs/core/application/command-handlers.md +617 -0
- package/docs/core/application/event-publishers.md +234 -0
- package/docs/core/application/event-translators.md +329 -0
- package/docs/core/application/index.md +99 -0
- package/docs/core/application/integration-events.md +277 -0
- package/docs/core/application/outbox.md +416 -0
- package/docs/core/application/query-handlers.md +292 -0
- package/docs/core/application/unit-of-work.md +352 -0
- package/docs/core/domain/aggregates.md +822 -0
- package/docs/core/domain/domain-errors.md +251 -0
- package/docs/core/domain/domain-events.md +292 -0
- package/docs/core/domain/domain-services.md +249 -0
- package/docs/core/domain/entities.md +431 -0
- package/docs/core/domain/index.md +93 -0
- package/docs/core/domain/ports.md +284 -0
- package/docs/core/domain/repositories.md +335 -0
- package/docs/core/domain/value-objects.md +425 -0
- package/docs/core/domain/views.md +265 -0
- package/docs/core/index.md +108 -0
- package/docs/core/strategic/anti-corruption-layers.md +349 -0
- package/docs/core/strategic/index.md +83 -0
- package/docs/core/strategic/open-host-services.md +287 -0
- package/docs/core/strategic/published-language.md +265 -0
- package/docs/core/utilities/result.md +413 -0
- package/docs/guide/agents.md +68 -0
- package/docs/guide/existing-project.md +108 -0
- package/docs/guide/getting-started.md +286 -0
- package/docs/guide/learning-path.md +123 -0
- package/docs/guide/project-layout.md +324 -0
- package/docs/guide/versioning.md +42 -0
- package/docs/integrations/index.md +112 -0
- package/docs/integrations/nestjs.md +169 -0
- package/docs/rules/index.md +185 -0
- package/docs/rules/layers/no-driving-shortcut.md +119 -0
- package/docs/rules/layers/no-impure-domain.md +191 -0
- package/docs/rules/layers/no-outward-import.md +186 -0
- package/docs/rules/layers/no-portless-adapter.md +123 -0
- package/docs/rules/strategic/no-cross-context-import.md +140 -0
- package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
- package/docs/rules/strategic/no-leaky-host-service.md +107 -0
- package/docs/rules/strategic/no-unmapped-context.md +114 -0
- package/docs/rules/tactical/no-aggregate-reference.md +139 -0
- package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
- package/docs/rules/tactical/no-foreign-query-dependency.md +201 -0
- package/docs/rules/tactical/no-loose-code.md +171 -0
- package/docs/rules/tactical/no-misplaced-class.md +146 -0
- package/docs/rules/tactical/no-public-field.md +113 -0
- package/docs/rules/tactical/no-stateful-service.md +102 -0
- package/docs/rules/tactical/no-thrown-failure.md +162 -0
- package/docs/rules/tooling/no-loose-disable.md +98 -0
- package/package.json +4 -3
- package/dist/cli-P5PwH9OE.mjs.map +0 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The application layer in Domain-Driven Design with TypeScript: command and query handlers that run use cases atomically and reliably."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Application
|
|
6
|
+
|
|
7
|
+
The application runs the use cases: one handler per request, which calls the domain and makes the
|
|
8
|
+
change atomic and reliable. It lives in `application/`; the adapters of its contracts live in
|
|
9
|
+
`driven/`.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
A controller that loads an order, checks it, saves it and sends an email mixes HTTP, rules and
|
|
14
|
+
storage. A second entry point, a consumer or a CLI, copies it. If the email leaves before the save
|
|
15
|
+
fails, the customer is told about an order that does not exist.
|
|
16
|
+
|
|
17
|
+
::: tip The fix
|
|
18
|
+
Each use case is one handler that only coordinates: load, call the domain, save, hand over the
|
|
19
|
+
events. The save and the events commit together, and the events are sent after.
|
|
20
|
+
:::
|
|
21
|
+
|
|
22
|
+
## How a request flows
|
|
23
|
+
|
|
24
|
+
<div class="al-diagram">
|
|
25
|
+
<svg viewBox="0 0 680 310" role="img" aria-label="A command goes from a controller to PlaceOrderHandler, which in one unit of work saves the order and adds the events translated by OrderEventsTranslator to the outbox. Later, OutboxRelay reads the outbox and the event publisher sends the integration events to other contexts. A query goes from a controller to GetOrderSummaryHandler, which reads a view through a query repository.">
|
|
26
|
+
<defs>
|
|
27
|
+
<marker id="application-overview-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
28
|
+
<path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
|
|
29
|
+
</marker>
|
|
30
|
+
</defs>
|
|
31
|
+
<text class="note" x="8" y="20">command</text>
|
|
32
|
+
<rect class="box" x="8" y="50" width="120" height="48" rx="8" />
|
|
33
|
+
<text class="label" x="68" y="70" text-anchor="middle">Controller</text>
|
|
34
|
+
<text class="note" x="68" y="88" text-anchor="middle">driving</text>
|
|
35
|
+
<rect class="boundary" x="146" y="28" width="526" height="80" rx="12" />
|
|
36
|
+
<text class="note" x="160" y="44">one unit of work</text>
|
|
37
|
+
<rect class="box" x="160" y="52" width="160" height="44" rx="8" />
|
|
38
|
+
<text class="label" x="240" y="70" text-anchor="middle">PlaceOrderHandler</text>
|
|
39
|
+
<text class="note" x="240" y="87" text-anchor="middle">command handler</text>
|
|
40
|
+
<rect class="box" x="342" y="52" width="160" height="44" rx="8" />
|
|
41
|
+
<text class="label" x="422" y="70" text-anchor="middle">OrderEventsTranslator</text>
|
|
42
|
+
<text class="note" x="422" y="87" text-anchor="middle">event translator</text>
|
|
43
|
+
<rect class="box" x="524" y="52" width="136" height="44" rx="8" />
|
|
44
|
+
<text class="label" x="592" y="70" text-anchor="middle">Outbox</text>
|
|
45
|
+
<text class="note" x="592" y="87" text-anchor="middle">with the save</text>
|
|
46
|
+
<path class="link" d="M 128 74 L 158 74" marker-end="url(#application-overview-arrow)" />
|
|
47
|
+
<path class="link" d="M 320 74 L 340 74" marker-end="url(#application-overview-arrow)" />
|
|
48
|
+
<path class="link" d="M 502 74 L 522 74" marker-end="url(#application-overview-arrow)" />
|
|
49
|
+
<text class="note" x="8" y="136">later</text>
|
|
50
|
+
<rect class="box" x="524" y="150" width="148" height="48" rx="8" />
|
|
51
|
+
<text class="label" x="598" y="170" text-anchor="middle">OutboxRelay</text>
|
|
52
|
+
<text class="note" x="598" y="188" text-anchor="middle">reads pending</text>
|
|
53
|
+
<rect class="box" x="342" y="150" width="160" height="48" rx="8" />
|
|
54
|
+
<text class="label" x="422" y="170" text-anchor="middle">EventPublisher</text>
|
|
55
|
+
<text class="note" x="422" y="188" text-anchor="middle">sends them</text>
|
|
56
|
+
<rect class="box" x="160" y="150" width="160" height="48" rx="8" />
|
|
57
|
+
<text class="label" x="240" y="170" text-anchor="middle">Other contexts</text>
|
|
58
|
+
<text class="note" x="240" y="188" text-anchor="middle">integration events</text>
|
|
59
|
+
<path class="link" d="M 598 108 L 598 148" marker-end="url(#application-overview-arrow)" />
|
|
60
|
+
<path class="link" d="M 524 174 L 504 174" marker-end="url(#application-overview-arrow)" />
|
|
61
|
+
<path class="link" d="M 342 174 L 322 174" marker-end="url(#application-overview-arrow)" />
|
|
62
|
+
<text class="note" x="8" y="226">query</text>
|
|
63
|
+
<rect class="box" x="8" y="244" width="120" height="48" rx="8" />
|
|
64
|
+
<text class="label" x="68" y="264" text-anchor="middle">Controller</text>
|
|
65
|
+
<text class="note" x="68" y="282" text-anchor="middle">driving</text>
|
|
66
|
+
<rect class="box" x="160" y="244" width="204" height="48" rx="8" />
|
|
67
|
+
<text class="label" x="262" y="264" text-anchor="middle">GetOrderSummaryHandler</text>
|
|
68
|
+
<text class="note" x="262" y="282" text-anchor="middle">query handler</text>
|
|
69
|
+
<rect class="box" x="396" y="244" width="200" height="48" rx="8" />
|
|
70
|
+
<text class="label" x="496" y="264" text-anchor="middle">OrderSummaries</text>
|
|
71
|
+
<text class="note" x="496" y="282" text-anchor="middle">query repository · a view</text>
|
|
72
|
+
<path class="link" d="M 128 268 L 158 268" marker-end="url(#application-overview-arrow)" />
|
|
73
|
+
<path class="link" d="M 364 268 L 394 268" marker-end="url(#application-overview-arrow)" />
|
|
74
|
+
</svg>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="al-cards">
|
|
78
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Change</span>A <a href="/core/application/command-handlers">command handler</a> loads an aggregate, calls it and saves it, inside a <a href="/core/application/unit-of-work">unit of work</a>.</div>
|
|
79
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Announce</span>An <a href="/core/application/event-translators">event translator</a> turns domain events into <a href="/core/application/integration-events">integration events</a>, stored in the <a href="/core/application/outbox">outbox</a> with the change.</div>
|
|
80
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Deliver</span>The relay hands them to an <a href="/core/application/event-publishers">event publisher</a>. A <a href="/core/application/query-handlers">query handler</a> reads, on its own path.</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
## The building blocks
|
|
84
|
+
|
|
85
|
+
| Building block | What it is | Use it when |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| [Command handlers](./command-handlers.md) | The application service of one use case that changes the system. | A request changes state: create, place, cancel. |
|
|
88
|
+
| [Query handlers](./query-handlers.md) | The application service of one read. | A request only reads. |
|
|
89
|
+
| [Event translators](./event-translators.md) | Turns domain events into integration events. | Other contexts must hear about a change. |
|
|
90
|
+
| [Integration events](./integration-events.md) | What other contexts receive when something happens: JSON. | You define what leaves your context. |
|
|
91
|
+
| [Event publishers](./event-publishers.md) | Sends integration events to the rest of the system. | You plug in a broker, or deliver in process. |
|
|
92
|
+
| [Unit of Work](./unit-of-work.md) | Makes a use case atomic: commit on success, roll back otherwise. | A use case writes more than once, or writes and records events. |
|
|
93
|
+
| [Outbox](./outbox.md) | Stores integration events with the change, then relays them, so none is lost. | Events must not be lost nor sent for a change that failed. |
|
|
94
|
+
|
|
95
|
+
## See also
|
|
96
|
+
|
|
97
|
+
- [Domain](../domain/index.md), what the handlers call
|
|
98
|
+
- [Strategic](../strategic/index.md), how contexts meet
|
|
99
|
+
- Rules: [`layers/no-outward-import`](../../rules/layers/no-outward-import.md), [`tactical/no-foreign-command-dependency`](../../rules/tactical/no-foreign-command-dependency.md), [`tactical/no-foreign-query-dependency`](../../rules/tactical/no-foreign-query-dependency.md)
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Integration events in Domain-Driven Design with TypeScript: versioned JSON messages that tell other bounded contexts what happened in yours."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Integration events
|
|
6
|
+
|
|
7
|
+
An integration event is what other bounded contexts receive when something happens in yours: plain
|
|
8
|
+
JSON with a name, a version and the operation it belongs to.
|
|
9
|
+
|
|
10
|
+
<dl class="al-glance">
|
|
11
|
+
<dt>Layer</dt><dd>Published language</dd>
|
|
12
|
+
<dt>File</dt><dd><code>published-language/order-placed.representation.ts</code></dd>
|
|
13
|
+
<dt>Type</dt><dd><a href="#api"><code>IntegrationEvent<Type, Payload></code></a></dd>
|
|
14
|
+
<dt>Built by</dt><dd><a href="/core/application/event-translators">Event translators</a></dd>
|
|
15
|
+
<dt>Used by</dt><dd><a href="/core/application/outbox">Outbox</a>, <a href="/core/application/event-publishers">Event publishers</a>, consumers in other contexts</dd>
|
|
16
|
+
<dt>Checked by</dt><dd><a href="/rules/strategic/no-cross-context-import"><code>strategic/no-cross-context-import</code></a>, <a href="/rules/layers/no-outward-import"><code>layers/no-outward-import</code></a></dd>
|
|
17
|
+
</dl>
|
|
18
|
+
|
|
19
|
+
## Why
|
|
20
|
+
|
|
21
|
+
When an order is placed, billing must know. The `OrderPlaced` domain event is a class holding an
|
|
22
|
+
`OrderId` and a `Date`: it cannot be stored in the outbox and read back as is, a broker cannot carry
|
|
23
|
+
it, and billing would have to import your classes to read it. Every change to your model would
|
|
24
|
+
become a change to billing.
|
|
25
|
+
|
|
26
|
+
::: tip The fix
|
|
27
|
+
An integration event is a JSON type you declare in your
|
|
28
|
+
[published language](../strategic/published-language.md). It is stored, carried and read as is, and
|
|
29
|
+
the consumer shares no code with you. The [domain event](../domain/domain-events.md) stays internal;
|
|
30
|
+
an [event translator](./event-translators.md) builds the integration event from it.
|
|
31
|
+
:::
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
Every integration event carries the same envelope around its payload:
|
|
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>Identity</span><code>id</code> is the id of the domain event. Consumers use it to ignore a duplicate.</div>
|
|
39
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Contract</span><code>type</code> names the event and <code>version</code> the shape of its payload.</div>
|
|
40
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Origin</span><code>source</code> is the bounded context, <code>occurredAt</code> the date, as an ISO 8601 string.</div>
|
|
41
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span>Tracing</span><code>correlationId</code> is the operation it belongs to; <code>causationId</code> the message that caused it.</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"id": "0b8f6c1e-5d2a-4c4e-9a51-3f7c2e1d9b40",
|
|
47
|
+
"type": "OrderPlaced",
|
|
48
|
+
"version": 1,
|
|
49
|
+
"source": "ordering",
|
|
50
|
+
"occurredAt": "2026-10-05T09:30:00.000Z",
|
|
51
|
+
"correlationId": "order-42",
|
|
52
|
+
"payload": { "orderId": "order-42", "customerId": "customer-7" }
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Where it fits
|
|
57
|
+
|
|
58
|
+
The integration event is born in the command handler, from a translated domain event, and travels
|
|
59
|
+
as JSON until another context reads it.
|
|
60
|
+
|
|
61
|
+
<div class="al-diagram">
|
|
62
|
+
<svg viewBox="0 0 680 150" role="img" aria-label="The event translator builds the integration event, the outbox stores it, the event publisher sends it, and the billing context consumes it. The integration event travels as JSON across all of them.">
|
|
63
|
+
<defs>
|
|
64
|
+
<marker id="integration-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
65
|
+
<path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
|
|
66
|
+
</marker>
|
|
67
|
+
</defs>
|
|
68
|
+
<rect class="box" x="8" y="24" width="150" height="56" rx="8" />
|
|
69
|
+
<text class="label" x="83" y="48" text-anchor="middle">Translator</text>
|
|
70
|
+
<text class="note" x="83" y="68" text-anchor="middle">builds it</text>
|
|
71
|
+
<path class="link" d="M 158 52 L 180 52" marker-end="url(#integration-flow-arrow)" />
|
|
72
|
+
<rect class="box" x="182" y="24" width="150" height="56" rx="8" />
|
|
73
|
+
<text class="label" x="257" y="48" text-anchor="middle">Outbox</text>
|
|
74
|
+
<text class="note" x="257" y="68" text-anchor="middle">stores it</text>
|
|
75
|
+
<path class="link" d="M 332 52 L 354 52" marker-end="url(#integration-flow-arrow)" />
|
|
76
|
+
<rect class="box" x="356" y="24" width="150" height="56" rx="8" />
|
|
77
|
+
<text class="label" x="431" y="48" text-anchor="middle">Event publisher</text>
|
|
78
|
+
<text class="note" x="431" y="68" text-anchor="middle">sends it</text>
|
|
79
|
+
<path class="link" d="M 506 52 L 528 52" marker-end="url(#integration-flow-arrow)" />
|
|
80
|
+
<rect class="box" x="530" y="24" width="142" height="56" rx="8" />
|
|
81
|
+
<text class="label" x="601" y="48" text-anchor="middle">Billing</text>
|
|
82
|
+
<text class="note" x="601" y="68" text-anchor="middle">reads it</text>
|
|
83
|
+
<rect class="boundary" x="8" y="100" width="664" height="36" rx="8" />
|
|
84
|
+
<text class="note" x="340" y="123" text-anchor="middle">this page: OrderPlaced v1, the same JSON all the way</text>
|
|
85
|
+
</svg>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
::: tip
|
|
89
|
+
Delivery is at least once: the same event may arrive twice. Consumers deduplicate by `id`.
|
|
90
|
+
:::
|
|
91
|
+
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import type {
|
|
96
|
+
IntegrationEvent,
|
|
97
|
+
PublishedLanguage,
|
|
98
|
+
} from "@alveolus/core";
|
|
99
|
+
// or: from "@alveolus/core/integration-events"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`IntegrationEvent` is a type: nothing exists at runtime. Values are built by an
|
|
103
|
+
[event translator](./event-translators.md).
|
|
104
|
+
|
|
105
|
+
### Type parameters
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
type IntegrationEvent<
|
|
109
|
+
Type extends string = string,
|
|
110
|
+
Payload extends JsonValue = JsonValue,
|
|
111
|
+
> = PublishedLanguage<{ … }>;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Parameter | What it is | Constraint |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| `Type` | The name of the event in the contract. | extends `string` |
|
|
117
|
+
| `Payload` | The data of the event. | extends `JsonValue` |
|
|
118
|
+
|
|
119
|
+
### Fields you declare
|
|
120
|
+
|
|
121
|
+
| Field | What it is |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| `type` | The name of the event in the contract, such as `"OrderPlaced"`. A string you choose, not a class name. |
|
|
124
|
+
| `payload` | The data of the event, in JSON only. |
|
|
125
|
+
|
|
126
|
+
### Fields filled for you
|
|
127
|
+
|
|
128
|
+
The [event translator](./event-translators.md) fills them through `wrap`.
|
|
129
|
+
|
|
130
|
+
| Field | Type | What it is |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| `id` | `string` | The id of the domain event, used to deduplicate. |
|
|
133
|
+
| `version` | `number` | The version of the payload schema, given to `wrap`. |
|
|
134
|
+
| `source` | `string` | The bounded context that emitted the event. |
|
|
135
|
+
| `occurredAt` | `string` | When the domain event happened, as an ISO 8601 string. |
|
|
136
|
+
| `correlationId` | `string` | The operation the event belongs to, across contexts. |
|
|
137
|
+
| `causationId` | `string`, optional | The message that caused this event. |
|
|
138
|
+
|
|
139
|
+
### `IntegrationEventContext` <Badge type="info" text="type" /> <Badge type="tip" text="passed by the command handler" />
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
interface IntegrationEventContext {
|
|
143
|
+
readonly correlationId: string;
|
|
144
|
+
readonly causationId?: string;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Passed to `translate`: its values become the `correlationId` and `causationId` of the event.
|
|
149
|
+
Import it from `@alveolus/core` as a type.
|
|
150
|
+
|
|
151
|
+
### `AnyIntegrationEvent` <Badge type="info" text="type" /> <Badge type="tip" text="used by the outbox and the publisher" />
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
type AnyIntegrationEvent = IntegrationEvent;
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Any integration event, whatever its type and payload.
|
|
158
|
+
|
|
159
|
+
::: warning Caveats
|
|
160
|
+
- One integration event per domain event: both share the same `id`.
|
|
161
|
+
- The payload type is constrained to `JsonValue`: a class, a `Date` or `undefined` does not
|
|
162
|
+
compile.
|
|
163
|
+
:::
|
|
164
|
+
|
|
165
|
+
## Usage
|
|
166
|
+
|
|
167
|
+
Build the event that ordering publishes when an order is placed. Each step shows the whole file: added lines are highlighted, replaced lines are struck out.
|
|
168
|
+
|
|
169
|
+
<div class="al-cards">
|
|
170
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-start-from-the-domain-event">Start from the domain event</a></span>Know what happened.</div>
|
|
171
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-name-it-in-the-contract">Name it in the contract</a></span>A type and a name that never change.</div>
|
|
172
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-publish-what-consumers-need">Publish what consumers need</a></span>Plain fields, nothing internal.</div>
|
|
173
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-mark-it-as-published-language">Mark it as published language</a></span>Say it is a contract.</div>
|
|
174
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-build-it-in-a-translator">Build it in a translator</a></span>One place turns events into JSON.</div>
|
|
175
|
+
<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>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
### 1. Start from the domain event
|
|
179
|
+
|
|
180
|
+
An integration event announces a [domain event](../domain/domain-events.md), here `OrderPlaced`, to other contexts: it exists first.
|
|
181
|
+
|
|
182
|
+
### 2. Name it in the contract
|
|
183
|
+
|
|
184
|
+
Other contexts depend on the name, not on a class: give the event a stable `type` string and a payload in JSON only. The file lives in `published-language/`, where other contexts look.
|
|
185
|
+
|
|
186
|
+
```ts [src/ordering/published-language/order-placed.representation.ts]
|
|
187
|
+
import type { IntegrationEvent } from "@alveolus/core";
|
|
188
|
+
|
|
189
|
+
export type OrderPlacedRepresentation = IntegrationEvent<
|
|
190
|
+
"OrderPlaced",
|
|
191
|
+
{ readonly orderId: string }
|
|
192
|
+
>;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 3. Publish what consumers need
|
|
196
|
+
|
|
197
|
+
A consumer cannot load the order: add every field it needs, as plain JSON values. An identifier or a `Date` would not compile.
|
|
198
|
+
|
|
199
|
+
```ts [src/ordering/published-language/order-placed.representation.ts]
|
|
200
|
+
import type { IntegrationEvent } from "@alveolus/core";
|
|
201
|
+
|
|
202
|
+
export type OrderPlacedRepresentation = IntegrationEvent<
|
|
203
|
+
"OrderPlaced",
|
|
204
|
+
{ readonly orderId: string } // [!code --]
|
|
205
|
+
{ readonly orderId: string; readonly customerId: string } // [!code ++]
|
|
206
|
+
>;
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 4. Mark it as published language
|
|
210
|
+
|
|
211
|
+
`PublishedLanguage` says that this type is a contract with other contexts: changing it means a new `version`, not an edit.
|
|
212
|
+
|
|
213
|
+
```ts [src/ordering/published-language/order-placed.representation.ts]
|
|
214
|
+
import type { IntegrationEvent } from "@alveolus/core"; // [!code --]
|
|
215
|
+
import type { // [!code ++]
|
|
216
|
+
IntegrationEvent, // [!code ++]
|
|
217
|
+
PublishedLanguage, // [!code ++]
|
|
218
|
+
} from "@alveolus/core"; // [!code ++]
|
|
219
|
+
|
|
220
|
+
export type OrderPlacedRepresentation = IntegrationEvent< // [!code --]
|
|
221
|
+
"OrderPlaced", // [!code --]
|
|
222
|
+
{ readonly orderId: string; readonly customerId: string } // [!code --]
|
|
223
|
+
export type OrderPlacedRepresentation = PublishedLanguage< // [!code ++]
|
|
224
|
+
IntegrationEvent< // [!code ++]
|
|
225
|
+
"OrderPlaced", // [!code ++]
|
|
226
|
+
{ readonly orderId: string; readonly customerId: string } // [!code ++]
|
|
227
|
+
> // [!code ++]
|
|
228
|
+
>;
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
This is the complete representation.
|
|
232
|
+
|
|
233
|
+
### 5. Build it in a translator
|
|
234
|
+
|
|
235
|
+
The [event translator](./event-translators.md#usage) builds the value, and the outbox stores it as this JSON:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"id": "0b9f6c1e-4d1a-4c8e-9a51-2f3e7d6b8a10",
|
|
240
|
+
"type": "OrderPlaced",
|
|
241
|
+
"version": 1,
|
|
242
|
+
"source": "ordering",
|
|
243
|
+
"occurredAt": "2026-10-06T09:30:00.000Z",
|
|
244
|
+
"correlationId": "o-42",
|
|
245
|
+
"payload": { "orderId": "o-42", "customerId": "c-7" }
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 6. Check it
|
|
250
|
+
|
|
251
|
+
Run the checks. Two rules keep the representation the way it is now:
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
npx alveolus arch check
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
<div class="al-cards">
|
|
258
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/strategic/no-cross-context-import"><code>no-cross-context-import</code></a></span>A consumer redeclares the fields it reads: it never imports this type.</div>
|
|
259
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-outward-import"><code>no-outward-import</code></a></span>The published language imports only its own types and the published-language types of core.</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
A consumer that imports it from ordering is reported:
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
src/shipping/published-language/order-placed.representation.ts
|
|
266
|
+
1 error strategic/no-cross-context-import: Imports the published
|
|
267
|
+
language of ordering: redeclare the fields you read in your
|
|
268
|
+
own published-language/.
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## See also
|
|
272
|
+
|
|
273
|
+
- [Event translators](./event-translators.md), which build integration events
|
|
274
|
+
- [Outbox](./outbox.md) and [Event publishers](./event-publishers.md), which store and send them
|
|
275
|
+
- [Domain events](../domain/domain-events.md), what they are built from
|
|
276
|
+
- [Published Language](../strategic/published-language.md), where they are declared
|
|
277
|
+
- Rules: [`strategic/no-cross-context-import`](../../rules/strategic/no-cross-context-import.md), [`layers/no-outward-import`](../../rules/layers/no-outward-import.md)
|