Alpha Version: You are viewing the ALPHA documentation. This is an experimental version and may contain breaking changes.
Skip to main content

OutboundTranslationSlice

For a short summary of OutboundTranslationSlice, see Reventless Components Overview.

Framework Implementation

This component follows the Reventless Component Structure Pattern, using separate files for interface definitions (OutboundTranslationSlice.res), builder logic (OutboundTranslationSlice_Builder.res), and callback/handler logic (OutboundTranslationSlice_Callback.res).

Overview

d2 diagram

The OutboundTranslationSlice implements the Event Modeling Translation pattern for outbound external communication. It listens to events from a shared DcbEventLog, accumulates outbound work items into a TODO list, translates each item by calling an external service, and optionally publishes a command back into the domain.

Event Modeling: The Outbound Translation Pattern

In Event Modeling, an Outbound Translation handles communication from the system to external services:

Event(s) --> TODO List --> Translator --> External System
--> Command (optional)

The key difference from AutomationSlice is that the "process" step involves an external call (async, may fail) rather than a deterministic command derivation. Each item is translated independently, allowing individual success or failure.

Purpose and Responsibilities

  • Responsibility: Collect outbound items from events; call external services for each item via the translate function; optionally publish commands back into the domain; track status with per-item retry semantics
  • In: Events from DcbEventLog (subscribed via EventCollector)
  • Out: External API calls (via translate); optional commands to CommandTopic (via publishJsons); TODO state synced to QueryDb

Comparison with SideEffectHandler

AspectSideEffectHandlerOutboundTranslationSlice
ArchitectureAggregate-based pluginsDCB-based plugins
State trackingNone -- fire-and-forgetTODO list with status (Pending/Processing/Completed/Failed)
RetryRelies on EventCollector retry (entire batch)Per-item retry with configurable max
IdempotencyNone -- replays cause duplicate callsDeduplication key prevents double-processing
VisibilityNo queryable stateQueryDb stores full processing history
Command emissionNeverOptional -- can publish commands back

Choose SideEffectHandler when you have plugins using Aggregates and need simple fire-and-forget event reactions.

Choose OutboundTranslationSlice when you need tracked, retryable external calls with full observability in a DCB-based plugin.

Component Spec

An OutboundTranslationSlice is split into two files:

  • <Name>.res — the spec (@@reventless.spec): the consumedEvent, outboundItem, and inboundCommand @schema types, the sweep config (maxRetries, heartbeatInterval), and targetName (None for fire-and-forget, or Some("<TargetSlice>") to publish a command back).
  • <Name>_Translation.res — the translation (@@reventless.translation): the collect function (event → outbound items) and the async translate function (the external call).

The spec module type the framework expects:

module type Spec = {
// name and moduleUrl are injected by @@reventless.spec — you never write them

@schema type consumedEvent
@schema type outboundItem
@schema type inboundCommand

let maxRetries: int
let heartbeatInterval: int
let targetName: option<string>
}

collect and translate live on the Translation module. There is no DcbEventLogSpec reference; the slice declares a local consumedEvent union.

Spec Fields Explained

FieldTypeDescription
consumedEvent@schema typeThe local subset of event variants this slice reacts to
outboundItem@schema typeData accumulated for each pending external call
inboundCommand@schema typeCommand type optionally published back after translate. Use unit for fire-and-forget
maxRetriesintMaximum retry attempts for failed items
heartbeatIntervalintSeconds between heartbeat sweeps for pending/failed items
targetNameoption<string>None for fire-and-forget; Some("<TargetSlice>") to route the optional command back

In the _Translation.res file:

FunctionTypeDescription
collectconsumedEvent => array<(string, outboundItem)>Map an event to zero or more outbound items (id + payload)
translate(string, outboundItem) => promise<result<...>>Call external service; returns success with optional command, or error

The translate Return Values

The translate function is the anti-corruption layer -- where user code calls external APIs:

  • Ok(Some((targetId, command))) -- External call succeeded; publish command back into the domain (e.g., confirm payment after calling payment gateway)
  • Ok(None) -- External call succeeded; no command needed (fire-and-forget, e.g., send email notification)
  • Error(msg) -- External call failed; item will be retried up to maxRetries

Usage Pattern

Example 1: Fire-and-Forget (Send Tracking Email)

The spec file. @@reventless.spec injects name, module Id, and moduleUrl from the filename, and inside a *Slice/ folder auto-applies DCB tags to *Id fields — never write @s.matches(...) by hand. targetName = None signals fire-and-forget:

Order/OutboundTranslationSlice/SendTrackingEmail.res
@@reventless.spec

@schema
type consumedEvent =
| OrderShipped({orderId: string, email: string})

@schema
type outboundItem = {orderId: string, email: string}

@schema
type inboundCommand = unit

let maxRetries = 3
let heartbeatInterval = 60
let targetName = None

The translation file (@@reventless.translation) holds collect and the async translate:

Order/OutboundTranslationSlice/SendTrackingEmail_Translation.res
@@reventless.translation

let collect = event =>
switch event {
| OrderShipped({orderId, email}) => [(orderId, {orderId, email})]
}

let translate = async (_id, item) => {
await EmailService.sendTrackingEmail(item.email, ~orderId=item.orderId)
Ok(None) // fire-and-forget: no command back
}

Example 2: Command-Back (Process Payment)

Here targetName = Some("ConfirmPayment") routes the optional command back into the domain:

Payment/OutboundTranslationSlice/ProcessPayment.res
@@reventless.spec

@schema
type consumedEvent =
| PaymentRequested({orderId: string, amount: float})

@schema
type outboundItem = {orderId: string, amount: float}

@schema
type inboundCommand = ConfirmPayment({
orderId: string,
transactionId: string,
})

let maxRetries = 5
let heartbeatInterval = 30
let targetName = Some("ConfirmPayment")
Payment/OutboundTranslationSlice/ProcessPayment_Translation.res
@@reventless.translation

let collect = event =>
switch event {
| PaymentRequested({orderId, amount}) => [(orderId, {orderId, amount})]
}

let translate = async (id, item) => {
try {
let result = await PaymentGateway.charge(item.amount, ~orderId=item.orderId)
Ok(Some((id, ConfirmPayment({orderId: item.orderId, transactionId: result.transactionId}))))
} catch {
| exn =>
let msg =
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("payment failed")
Error(msg)
}
}

Plugin Wiring

You never register or wire OutboundTranslationSlices by hand. The plugin generator scans the OutboundTranslationSlice/ folder and emits the wiring into the generated Plugin.res using the two-arg factory Platform.OutboundTranslationSlice.Make(Spec, Translation):

src/Plugin.res (generated — do not edit)
module Make = (Platform: ReventlessInfra.Platform.T) => {
// OutboundTranslationSlices
module SendTrackingEmailSlice = Platform.OutboundTranslationSlice.Make(SendTrackingEmail, SendTrackingEmail_Translation)
module ProcessPaymentSlice = Platform.OutboundTranslationSlice.Make(ProcessPayment, ProcessPayment_Translation)

let make = (~uiBundleUrl=?) =>
Platform.Plugin.make(
~name="Ordering",
~outboundTranslationSlices=[module(SendTrackingEmailSlice), module(ProcessPaymentSlice)],
// ... other components
)
}

The framework automatically wires the slice to the shared DcbEventLog and CommandTopic.

Runtime Behavior

Two-Phase Processing

The OutboundTranslationSlice callback has two phases that execute on each event batch:

d2 diagram

Phase 1 -- Collect (runs for each event in the batch):

for each event:
for each (id, item) in collect(event):
if not exists in TODO list:
insert {id, item, status: Pending}

Phase 2 -- Translate (runs after Phase 1, processes each item independently):

for each item where status = Pending
OR (status = Failed AND retryCount < maxRetries):
mark status = Processing
match await translate(id, item):
Ok(Some(targetId, cmd)) -> publish command, mark Completed
Ok(None) -> mark Completed (fire-and-forget)
Error(msg) -> mark Failed, increment retryCount

TODO Item Lifecycle

d2 diagram

Each TODO item moves through these statuses:

StatusDescription
PendingCreated by collect, waiting to be translated
Processingtranslate is being called
CompletedExternal call succeeded
FailedExternal call failed -- eligible for retry

Heartbeat Handler

A periodic heartbeat (configurable via heartbeatInterval) runs Phase 2 only, catching:

  • Failed items eligible for retry (retryCount < maxRetries)
  • Items collected in a previous batch but not yet translated

TODO List Storage

The TODO list is stored in a QueryDb for observability. Each row:

type todoStatus = Pending | Processing | Completed | Failed

type todoRow = {
item: JSON.t,
status: todoStatus,
createdAt: string,
processedAt?: string,
completedAt?: string,
retryCount: int,
lastError?: string,
}

This QueryDb is automatically created by the builder and can be queried via the GraphQL API to inspect pending work and translation history.

Error Handling

Phase 1 Errors (collect):

  • Event decoding failures are logged and skipped
  • collect is a pure function -- exceptions are unexpected but caught

Phase 2 Errors (translate):

  • outboundItem decoding failures are logged, item skipped
  • translate exceptions are caught and treated as Error(msg)
  • Command encoding failures mark item as Failed with incremented retryCount
  • Publishing failures mark item as Failed for retry
  • Individual item failure does not affect other items

Recovery:

  • Failed items are retried up to maxRetries times
  • Heartbeat sweeps pick up items that need retry
  • All errors logged with slice name and context

Pulumi Outputs

type outputs = {
resources: array<Adapter.resource>,
queryDb: QueryDb.outputs,
}

type operations = {
enqueueEvent: EventCollector.enqueueEvent,
translatePending: unit => promise<unit>,
}

Resource Naming:

  • Component type: reventless:OutboundTranslationSlice
  • TODO list QueryDb: {name}Todo
  • EventCollector: subscribed to DcbEventLog's EventTopic

Dependencies:

  • DcbEventLog (shared event storage)
  • CommandTopic (via publishJsons for optional command-back publishing)
  • QueryDb (for TODO list persistence)
  • AutomationSlice -- Similar TODO list pattern for internal command automation (no external calls)
  • InboundTranslationSlice -- Complementary component for receiving external input
  • SideEffectHandler -- Simpler fire-and-forget pattern for plugins using Aggregates
  • DcbEventLog -- Shared event log that OutboundTranslationSlice subscribes to
  • StateChangeSlice -- Processes the commands OutboundTranslationSlice optionally produces
  • CommandTopic -- Receives optional commands from the translator
  • EventCollector -- Subscribes to DcbEventLog events
  • QueryDb -- Stores the TODO list for observability
  • Plugin -- Hosts OutboundTranslationSlice via DcbSpec