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

InboundTranslationSlice

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

Framework Implementation

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

Overview

d2 diagram

The InboundTranslationSlice implements the Event Modeling Translation pattern for inbound external communication. It receives external input (webhooks, API calls, message queue messages), validates and transforms it through an anti-corruption layer, and publishes domain commands.

Event Modeling: The Inbound Translation Pattern

In Event Modeling, an Inbound Translation handles communication from external services into the system:

External Input --> Anti-Corruption Layer (translate) --> Command --> Event(s)

The anti-corruption layer protects the domain from external data formats and validates input before it enters the system.

Purpose and Responsibilities

  • Responsibility: Receive external input; validate against a schema; translate to domain commands via an anti-corruption layer; publish commands to the shared CommandTopic; maintain an audit log of all translation attempts
  • In: External JSON input (via operations.receive)
  • Out: Commands to CommandTopic (via publishJsons); audit records to QueryDb
  • Key Feature: Unlike other slices, InboundTranslationSlice is triggered externally via operations.receive rather than by subscribing to domain events

Comparison with Task

AspectTaskInboundTranslationSlice
TriggerS3 object / scheduleHTTP webhook / API / message queue
Input validationNone -- raw JSONAnti-corruption layer with schema validation
TranslationAd-hocStructured translate function with typed input/output
Error handlingLambda errorStructured error response to caller
ObservabilityCloudWatch onlyQueryDb audit log of all translations

Choose Task when you need S3 triggers, scheduled jobs, or provider-specific integrations.

Choose InboundTranslationSlice when you need validated, audited external input processing with a clean anti-corruption layer.

Component Spec

An InboundTranslationSlice is split into two files:

  • <Name>.res — the spec (@@reventless.spec): the externalInput and command @schema types, plus targetName (the aggregate or StateChangeSlice that receives the produced command).
  • <Name>_Translation.res — the translation (@@reventless.translation): the synchronous translate function (the anti-corruption layer).

The spec module type the framework expects:

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

@schema type externalInput
@schema type command

let targetName: string
let commandAuthorization: command => Authorization.permission
}

translate lives on the Translation module. There is no DcbEventLogSpec reference. @@reventless.spec injects name, moduleUrl, and a default commandAuthorization (AllowAuthenticated).

Spec Fields Explained

FieldTypeDescription
externalInput@schema typeThe external data format received from the outside world
command@schema typeThe domain command produced by the anti-corruption layer
targetNamestringName of the aggregate or StateChangeSlice that receives the produced command

The translate Function

The translate function lives in the _Translation.res file and is the anti-corruption layer -- it protects the domain from external data formats. It returns result<array<(string, command)>, string>:

  • Ok([(targetId, command), ...]) -- Input is valid; publish one or more commands to the target entities
  • Ok([]) -- Idempotent no-op; nothing to publish
  • Error(msg) -- Input is invalid or cannot be translated; return error to caller

The function is synchronous (no external calls needed) since all the external interaction already happened when the input was received.

Usage Pattern

Example 1: Payment Webhook

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

Payment/InboundTranslationSlice/PaymentWebhook.res
@@reventless.spec

@schema
type externalInput = {
paymentId: string,
orderId: string,
status: string,
amount: float,
}

@schema
type command = ConfirmPayment({
orderId: string,
paymentId: string,
amount: float,
})

let targetName = "ConfirmPayment"

The translation file (@@reventless.translation) holds the synchronous translate, returning an array of (targetId, command) pairs:

Payment/InboundTranslationSlice/PaymentWebhook_Translation.res
@@reventless.translation

let translate = (input: externalInput) =>
switch input.status {
| "completed" =>
Ok([
(
input.orderId,
ConfirmPayment({
orderId: input.orderId,
paymentId: input.paymentId,
amount: input.amount,
}),
),
])
| "refunded" =>
// Could map to a different command variant
Error("Refund handling not yet implemented")
| status => Error("Unknown payment status: " ++ status)
}

Example 2: Shipping Update Webhook

Shipping/InboundTranslationSlice/ShippingUpdate.res
@@reventless.spec

@schema
type externalInput = {
trackingId: string,
orderId: string,
event: string,
timestamp: string,
}

@schema
type command = UpdateShipmentStatus({
orderId: string,
trackingId: string,
status: string,
})

let targetName = "UpdateShipmentStatus"
Shipping/InboundTranslationSlice/ShippingUpdate_Translation.res
@@reventless.translation

let translate = (input: externalInput) =>
switch input.event {
| "picked_up" | "in_transit" | "delivered" =>
Ok([
(
input.orderId,
UpdateShipmentStatus({
orderId: input.orderId,
trackingId: input.trackingId,
status: input.event,
}),
),
])
| event => Error("Unrecognized shipping event: " ++ event)
}

Plugin Wiring

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

src/Plugin.res (generated — do not edit)
module Make = (Platform: ReventlessInfra.Platform.T) => {
// InboundTranslationSlices
module PaymentWebhookSlice = Platform.InboundTranslationSlice.Make(PaymentWebhook, PaymentWebhook_Translation)
module ShippingUpdateSlice = Platform.InboundTranslationSlice.Make(ShippingUpdate, ShippingUpdate_Translation)

let make = (~uiBundleUrl=?) =>
Platform.Plugin.make(
~name="Ordering",
~inboundTranslationSlices=[module(PaymentWebhookSlice), module(ShippingUpdateSlice)],
// ... other components
)
}

Runtime Behavior

Receive Flow

Unlike other slices, InboundTranslationSlice is triggered externally via operations.receive:

d2 diagram

Receive processing steps:

receive(inputJson):
1. Parse inputJson against Spec.externalInputSchema
-> Error: record audit failure, return Error(msg)

2. Call Spec.translate(input) -- anti-corruption layer
-> Error(msg): record audit failure, return Error(msg)

3. Encode command via Spec.commandSchema
-> Error: record audit failure, return Error(msg)

4. Publish command via publishJsons
-> Error: record audit failure, return Error(msg)

5. Record audit success, return Ok(targetId)

Integration: Exposing the Endpoint

The InboundTranslationSlice exposes operations.receive which accepts JSON.t and returns promise<result<string, string>>. You need to wire this to an HTTP endpoint:

Option A -- GraphQL Mutation (consistent with existing Api component):

// Register a GraphQL mutation resolver that calls operations.receive
let resolver = async (inputJson) => {
switch await inboundSliceOps.receive(inputJson) {
| Ok(targetId) => {success: true, targetId}
| Error(msg) => {success: false, error: msg}
}
}

Option B -- Lambda Function URL / API Gateway (for external webhooks):

// Lambda handler that accepts webhook POST body
let handler = async (event) => {
let body = event.body->JSON.parseExn
switch await inboundSliceOps.receive(body) {
| Ok(_) => {statusCode: 200, body: "OK"}
| Error(msg) => {statusCode: 400, body: msg}
}
}

Both options can coexist -- the component exposes operations.receive and the transport layer is up to you.

Audit Log

All translation attempts are recorded in a QueryDb for observability:

type auditStatus = Success | Failure

type auditRow = {
input: JSON.t,
status: auditStatus,
targetId?: string,
error?: string,
receivedAt: string,
}

The audit log records every call to receive, whether successful or not, providing a complete history of all external input processed by the slice.

Error Handling

Input Parsing Errors:

  • Invalid JSON or schema mismatch returns Error to the caller
  • Audit log records the failure with the raw input

Translation Errors:

  • translate returning Error(msg) is the normal rejection path (e.g., unknown status code)
  • Audit log records the failure with the parsed input

Publishing Errors:

  • Command encoding failures return Error to the caller
  • publishJsons failures return Error to the caller
  • Audit log records the failure

All errors are returned to the caller as Error(msg), allowing the external system to retry or handle the failure.

Pulumi Outputs

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

type operations = {
receive: JSON.t => promise<result<string, string>>,
}

Resource Naming:

  • Component type: reventless:InboundTranslationSlice
  • Audit log QueryDb: {name}Audit

Dependencies:

  • CommandTopic (via publishJsons for command publishing)
  • QueryDb (for audit log persistence)
  • OutboundTranslationSlice -- Complementary component for calling external services
  • StateChangeSlice -- Processes the commands InboundTranslationSlice produces
  • DcbEventLog -- Shared event log that receives events from processed commands
  • CommandTopic -- Receives commands from the translator
  • QueryDb -- Stores the audit log
  • Task -- Alternative for S3/schedule-triggered external processing
  • Plugin -- Hosts InboundTranslationSlice via DcbSpec