integration-event ยท diff

git:20261010.ec77307 to git:20261010.c6c87e0

63 added, 53 removed. Audit A to A.

---
name: integration-event
description: >-
- Makes two bounded contexts collaborate through a versioned integration event on any
- provider (in-memory, outbox and broker, queue service): declaring and publishing,
- consuming idempotently, evolving a payload, plugging a new provider. Use when a context
- must react to another, or when the user mentions events between contexts, messaging,
- broker, outbox, Kafka, RabbitMQ, SQS, NATS or eventual consistency.
+ Makes two bounded contexts collaborate through a versioned integration event: declaring it
+ in the producer's published language, publishing through the outbox, handling it
+ idempotently in the consumer, evolving its payload, choosing a broker. Use when a context
+ must react to another, when building a contract row of the slices, or when the user
+ mentions events between contexts, messaging, outbox, consumers, RabbitMQ, Kafka, NATS,
+ Redis Streams or eventual consistency. To decide who is upstream and which pattern, use
+ context-map first.
argument-hint: "<producer-context> <Aggregate> <Fact>"
allowed-tools:
+ - Read
+ - Edit
- Bash(npx ddd-ts-kit generate *)
- Bash(npx ddd-ts-kit check *)
- Bash(npx ddd-ts-kit list *)
- Bash(npm run events:generate)
- Bash(npm run events:check)
+ - Bash(npm test *)
---
# Integration events
Event under discussion: `$ARGUMENTS`
- Domain events stay inside their context. What crosses a boundary is an **integration event**:
- a contract the producer owns, in its `published-language/`.
+ A domain event stays inside its context. What crosses a boundary is an integration event: a
+ contract the producer owns, in its `published-language/`, the only folder another context
+ may import.
- ## Publish
+ ## Publish (producer side)
```bash
npx ddd-ts-kit generate integration-event <ctx> <Aggregate> <Fact> --json
```
- - The event is declared once, in `published-language/`, with `defineIntegrationEvent({ type,
- version, payload })` from `@ddd-ts-kit/events`. The declaration types the payload, validates
- what is published and consumed, and feeds the generated catalog and AsyncAPI document
- (`npm run events:generate`, checked in CI by `events:check`). Describe each payload field in
- the schema: the description lands in the catalog.
- - Name: `<context>.<aggregate>.<fact>`, past tense, kebab-case.
- - Payload: what consumers need to act without calling back, and nothing internal. Identifiers
- and facts, not the aggregate dumped as JSON. No personal data unless a consumer needs it.
- - The publisher translates the domain event into the envelope after the unit of work commits.
- With a database, that means the transactional outbox of `@ddd-ts-kit/events`
- (`OutboxPublisher`, `OutboxRelay`, a store from the ORM package): the event is written in
- the transaction of the command and relayed afterwards. Publishing straight to a broker inside a handler loses events.
+ writes the declaration `published-language/<fact>.v1.integration-event.ts`
+ (`defineIntegrationEvent({ type, version, payload })`), its export, the publisher in
+ `application/integration/publishers/` (a reaction to the domain event, which it creates when
+ missing) and the specs. Fill the `next` places:
- The outbox is not optional once there is a database (`events/outbox-required`). What depends
- on the transport is the relay and the consumer side: polling or database notification in one
- process, publisher confirms and dead-letter exchange on a queue broker, keyed partitions or
- change data capture on a log. The table is in `docs/ARCHITECTURE.md` ("Outbox, by transport")
- and each provider package documents its strategy.
+ - **Name**: `<context>.<aggregate>.<fact>`, past tense (`events/name-format`).
+ - **Payload**: what a consumer needs to act without calling back, and nothing internal:
+ identifiers and facts, not the aggregate as JSON; no personal data unless a consumer needs
+ it. Describe each field in the schema: the description lands in the catalog.
+ - **Outbox**: with a database the event is written in the transaction of the command and
+ relayed afterwards; a publisher depends on `IntegrationEventPublisher` and nothing else
+ (`events/outbox-required`). Never publish to a broker from a handler.
- ## Consume
+ The payload is a contract between two owners: agree on it before the consumer is built, and
+ commit it first (skill `ddd-ts-kit:parallel-work`).
- Declare the relationship first (skill `ddd-ts-kit:model-context`), then:
+ ## Handle (consumer side)
+ The relationship is declared first (skill `ddd-ts-kit:context-map`); the generator refuses
+ otherwise.
+
```bash
npx ddd-ts-kit generate integration-handler <ctx> <upstream> <event-type> --json
```
- - An integration event handler translates the event into a command of its own context and sends it on the
- `CommandBus`. No business logic in it.
- - Delivery is at-least-once and unordered across aggregates: wrap with `idempotent(...)`, and
- make the command safe to replay.
- - It imports only the upstream's `published-language/`, and is registered under a stable,
- unique `consumer` name: the identity of its subscription.
- - Failure: let it throw so the provider retries; a poison message goes to a dead-letter
- destination, never swallowed.
+ writes `application/integration/handlers/<name>.integration-handler.ts` and its spec.
- ## Evolve
+ - The handler translates the event into **one command of its own context** and sends it on
+ the `CommandBus`, as `systemActor('<context>.<name>')`. No business logic in it.
+ - It is registered under a stable, unique `consumer` name, a string literal: the identity of
+ its subscription and of its idempotency mark (`events/consumer-idempotent`). Renaming it
+ replays or loses messages.
+ - Delivery is at least once and unordered across aggregates: the mark and the work share one
+ unit of work, and the command must be safe to replay.
+ - Let a failure throw so the provider retries; after bounded retries the message is buried
+ in a dead-letter destination and reported, never swallowed.
+ - A foreign model the context does not want is translated first:
+ `npx ddd-ts-kit generate acl <ctx> <upstream>`.
- Inside a version, changes are additive and optional. Anything else (removal, rename, type or
- meaning change) is a new `version`, published alongside the old one until every consumer
- has moved. Consumers ignore unknown fields.
+ ## Evolve
- ## Providers
+ Inside a version, changes are additive and optional; consumers ignore unknown fields.
+ Anything else (removal, rename, change of type or meaning) is
+ `generate integration-event ... --version <n+1>`, published next to the old version until
+ every consumer has moved.
- The application knows two ports from `@ddd-ts-kit/kernel`: `IntegrationEventPublisher` and
- `IntegrationEventSubscriber`. `InMemoryEventBus` implements both and is the default for tests,
- local development and single-process deployments. Another provider is an adapter in
- `src/platform/events/` implementing the same two ports, selected in `main.ts` from
- configuration. Requirements for any adapter:
+ ## Provider
- 1. serialize the envelope as is, `traceparent` included, so traces continue across the hop;
- 2. acknowledge only after the handler resolved;
- 3. route by `type`; one durable subscription per consumer name;
- 4. pass `eventBusContract` from `@ddd-ts-kit/testing/contracts`, the suite `InMemoryEventBus`
- passes too.
+ The application knows `IntegrationEventPublisher` and `IntegrationEventSubscriber` only. The
+ provider is `stack.broker`, wired in `src/platform/events/`: in memory by default (tests,
+ one process), or `@ddd-ts-kit/rabbitmq`, `@ddd-ts-kit/kafka`, `@ddd-ts-kit/nats`,
+ `@ddd-ts-kit/redis/event-bus`, each with one durable subscription per consumer, bounded
+ retries and dead letters. Relay and consumer strategy per transport:
+ `docs/ARCHITECTURE.md`, "Outbox, by transport", and each package README. A `custom` provider
+ passes `eventBusContract` of `@ddd-ts-kit/testing/contracts`. Handlers do not change with
+ the provider.
- Wrap the chosen adapter with `tracedPublisher` and `tracedSubscriber` from
- `@ddd-ts-kit/tracing` in the composition root; nothing else changes.
+ ## Finish
- Finish with `npx ddd-ts-kit check --rule events/name-format --rule context/published-language-only`.
+ ```bash
+ npm run events:generate # catalog and AsyncAPI, committed with the code
+ npx ddd-ts-kit check --rule events/name-format --rule events/defined-in-published-language \
+ --rule events/consumer-idempotent --rule context/published-language-only
+ ```