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

Adapter Pattern

The Reventless framework separates deploy-time (Pulumi infrastructure creation) from runtime (Lambda execution). Cloud provider packages implement this separation for each component.

The Two-Layer Pattern

Every component adapter consists of two layers:

Deploy-Time Layer

Creates cloud resources and captures their identifiers (ARNs, URLs, table names) as Pulumi.Output.t values.

// Example: EventLog storage adapter (deploy time)
module Make = (Spec: Reventless.EventLog.Spec) => {
// Creates the DynamoDB table during `pulumi up`
let table = aws.dynamodb.Table.make(~name=Spec.name, ...)

// Returns the table ARN wrapped in Pulumi.Output.t
let tableArn: Pulumi.Output.t<string> = table.arn
}

Runtime Layer

Implements the actual data operations. Uses the Pulumi.Output.t values captured during deploy time — Pulumi serializes these into the Lambda bundle with their resolved values.

// Example: EventLog storage runtime
let append = (~events, ~condition) => async {
// tableArn is captured from deploy time; resolved at Lambda startup
let client = DynamoDB.makeClient()
await client->putItem(~tableName=tableArn, ...)
}

Adapter Interfaces Checklist

The following adapter interfaces must be implemented for a complete provider package. Each is defined in packages/reventless/src/components/.

Core Event Sourcing

InterfaceFilePurpose
EventLog_Adapter.StorageEventLog/EventLog_Adapter.resAppend-only event log with optimistic concurrency
EventTopic_Adapter.PublisherEventTopic/EventTopic_Adapter.resPub/sub event delivery
CommandTopic_Adapter.ChannelCommandTopic/CommandTopic_Adapter.resCommand queue with FIFO ordering

Read Models

InterfaceFilePurpose
EventCollector_Adapter.ChannelEventCollector/EventCollector_Adapter.resSubscribe to event topics
QueryDb_Adapter.StorageQueryDb/QueryDb_Adapter.resKey-value store for projections
QueryDb_Adapter.QueryEngineAdapterQueryDb/QueryDb_Adapter.resQuery execution (scan, filter)

Supporting Services

InterfaceFilePurpose
Task_Adapter.StorageTask/Task_Adapter.resBinary object storage
Heartbeat_Adapter.RunnerHeartbeat/Heartbeat_Adapter.resScheduled health checks
Counter_Adapter.StorageCounter/Counter_Adapter.resAtomic counters

DCB (Dynamic Consistency Boundary)

InterfaceFilePurpose
DcbEventLog_Adapter.StorageDcbEventLog/DcbEventLog_Adapter.resTag-based event log

Runtime Environment

The runtime environment adapter provides the Lambda execution context. It is defined in src/adapter/Runtime/Runtime.res in the reventless package.

module type Environment = {
let makeHandler: (
~handler: Runtime.eventHandler<'event, 'context, 'result>,
) => Lambda.handler<'event, 'context, 'result>
}

Cloud provider packages provide their own Runtime.Environment that adapts the generic handler type to the specific Lambda invocation model (e.g., SQS event format for AWS).

Pre-Configured Builder Modules

After implementing all adapter interfaces, export pre-configured builder modules that combine the framework builder functors with your adapters:

// ReventlessMyprovider.res — the package entry point
module Aggregate = {
module Make = (Spec: Reventless.Aggregate.Spec) =>
Aggregate_Builder.Make(Spec, MyRuntimeEnvironment, MyEventLogStorage, MyEventTopicPublisher, MyCommandTopicChannel)
}

module ReadModel = {
module Make = (Spec: Reventless.ReadModel.Spec) =>
ReadModel_Builder.Make(Spec, MyRuntimeEnvironment, MyEventCollectorChannel, MyQueryDbStorage, MyQueryEngineAdapter)
}

// ... etc

This is the API surface for application developers. They call ReventlessMyprovider.Aggregate.Make(MySpec) and get a fully wired Aggregate.T.

InMemory Adapters

The reventless-local package implements the same adapter interfaces using simple in-process data structures. It dramatically simplifies local development and testing:

  • No Pulumi — resources are plain ReScript records, not Pulumi.Output.t<'a> wrappers
  • No AWS — event logs, queues, and buckets are in-memory maps and arrays
  • Synchronous — no async infrastructure, making tests deterministic
  • Resettable — each test can start from a clean state
// InMemory EventLog adapter — no Pulumi.Output.t wrapping needed
let make = (~name, ~opts=?) => {
let store: ref<array<event>> = ref([])

{
resources: [], // no cloud resources
operations: Pulumi.Output.make({ // still wrapped for API compatibility
append: async (_, _, jsons) => {
store := store.contents->Array.concat(jsons)
Ok()
},
replay: async id => Ok(store.contents->Array.filter(e => e.id == id)),
}),
}
}

The Local package is used by all framework tests. See the Local Provider for usage instructions.