Event translators
An event translator turns the domain events of an aggregate into integration events: plain JSON in the published language of the bounded context. It is the one place where the internal model meets the contract other contexts read.
export class OrderEventsTranslator extends EventTranslator<OrderPlaced, OrderPlacedRepresentation> {
protected readonly source = "ordering";
translate(event: OrderPlaced, context: IntegrationEventContext): OrderPlacedRepresentation {
return this.wrap(event, context, { payload: { orderId: event.aggregateId.value, total: event.payload.total }, type: "OrderPlaced", version: 1 });
}
}When to use
Write one translator per aggregate whose events leave the bounded context. The domain keeps its own types, such as identifiers and value objects; the translator decides what is published and in which form, so a refactoring of the domain never reaches other contexts by accident.
Usage
Translate the events of an aggregate
Event is the union of the domain events of the aggregate, Output the union of the integration events declared in the published language. Tell the events apart with instanceof, and wrap each one: wrap copies id and occurredAt from the domain event, adds source and the context, and takes the type, version and payload you give.
import { EventTranslator, type IntegrationEventContext } from "@alveolus/core";
import { OrderPlaced } from "../../domain/events/order-placed.event";
import type { OrderEvent } from "../../domain/events/order.event";
import type { OrderingEvent } from "../../published-language/ordering-event.representation";
export class OrderEventsTranslator extends EventTranslator<OrderEvent, OrderingEvent> {
protected readonly source = "ordering";
translate(event: OrderEvent, context: IntegrationEventContext): OrderingEvent {
if (event instanceof OrderPlaced) {
return this.wrap(event, context, {
payload: { currency: event.payload.total.currency, orderId: event.aggregateId.value, total: event.payload.total.amount },
type: "OrderPlaced",
version: 1,
});
}
return this.wrap(event, context, { payload: { orderId: event.aggregateId.value }, type: "OrderCancelled", version: 1 });
}
}OrderingEvent is the union of the representations, such as OrderPlacedRepresentation | OrderCancelledRepresentation: TypeScript rejects a type or a payload the contract does not declare.
Call it from the command handler
Inject the translator and map the events pulled from the aggregate, inside the unit of work, after saving.
await this.orders.save(order);
await this.outbox.add(order.pullDomainEvents().map((event) => this.translator.translate(event, { correlationId: orderId })));Keep domain objects out of the payload
The payload is JSON: identifiers and value objects are written as strings and numbers.
payload: { order: event.aggregateId, total: event.payload.total }payload: { orderId: event.aggregateId.value, total: event.payload.total.amount }The payload type is constrained to JsonValue: an Identifier or a Money does not compile.
Chain operations
When the change was caused by another message, pass its id as causationId and keep its correlationId: the whole chain can be followed across contexts.
this.translator.translate(event, { causationId: received.id, correlationId: received.correlationId });Reference
abstract class EventTranslator<Event extends AnyDomainEvent, Output extends AnyIntegrationEvent = AnyIntegrationEvent> {
protected abstract readonly source: string;
abstract translate(event: Event, context: IntegrationEventContext): Output;
protected wrap<Type extends string, Payload extends JsonValue>(
event: Event,
context: IntegrationEventContext,
contract: { readonly type: Type; readonly version: number; readonly payload: Payload },
): IntegrationEvent<Type, Payload>;
}| Type parameter | Description |
|---|---|
Event | The domain events it translates. |
Output | The integration events it produces. Defaults to any integration event. |
| Member | Type | Description |
|---|---|---|
source | string, protected abstract | The bounded context the events come from. |
translate(event, context) | Output | Turns one domain event into its integration event. |
wrap(event, context, contract) | IntegrationEvent<Type, Payload>, protected | Builds the integration event from { type, version, payload }, with id, occurredAt, source, correlationId and causationId filled in. |
Caveats
occurredAtbecomes an ISO 8601 string, andcausationIdis only set when the context has one.- The translator lives in the application layer: it reads the domain and the published language, the domain never knows it.
Import from @alveolus/core or @alveolus/core/event-translators.
See also
- Integration events, what it produces
- Published Language, where the contracts are declared
- Outbox, where the translated events go