Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScriptTutorial: Explicit Dependencies First
This tutorial starts one step before Flow<'env, 'error, 'value>. Define small interfaces, pass them explicitly, and compose a few operations before introducing an environment record.
Use this approach first when:
- the workflow is still local to one feature
- you want to prove the dependency boundaries before choosing an environment shape
- you want direct tests without building an environment first
1. Define The Contract
Show page setup
// Setup for the checked examples on this page.
open System
open System.Threading
open System.Threading.Tasks
open Axial
/// Fails the docs test when an example's result differs from the value shown.
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actualopen System
open System.Threading.Tasks
type OrderId = OrderId of Guid
type Order =
{ Id: OrderId
Email: string
Total: decimal }
type PlaceOrderError =
| InvalidEmail
| OrderRejected of string
type IOrderRepository =
abstract Save : Order -> Task<Result<unit, string>>
type IEmailSender =
abstract SendConfirmation : Order -> Task
SystemThreadingTasksFsLiveDocsGeneratedPage22_2D306DEB1823.OrderIdOrderIdSystem.GuidRepresents a globally unique identifier (GUID).
FsLiveDocsGeneratedPage22_2D306DEB1823.OrderId: OrderIdEmail: stringstringAn abbreviation for the CLI type . Basic Types
Total: decimaldecimalAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage22_2D306DEB1823.PlaceOrderErrorInvalidEmailOrderRejectedFsLiveDocsGeneratedPage22_2D306DEB1823.IOrderRepositorySave: IOrderRepository -> Order -> Task<Result<unit,string>>System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
FsLiveDocsGeneratedPage22_2D306DEB1823.IEmailSenderSendConfirmation: IEmailSender -> Order -> TaskSystem.Threading.Tasks.TaskRepresents an asynchronous operation.
Requires & types
Requires: open System; open System.Threading.TasksThese interfaces are intentionally narrow. They represent what the workflow needs, not the full database or mail client API.
PlaceOrderError deliberately has no timeout or cancellation case: this workflow owns no cancellation source or deadline, so a cancelled run surfaces as an interruption (matched with Cause.isInterrupted in step 5), and a timeout would only become a typed error if the workflow owned its deadline.
2. Compose Small Flows
let validateOrder (order: Order) : Result<Order, PlaceOrderError> =
if String.IsNullOrWhiteSpace order.Email then
Error InvalidEmail
else
Ok order
let saveOrder (orders: IOrderRepository) (order: Order) : Flow<unit, PlaceOrderError, Order> =
flow {
do!
Flow.fromTaskResult(fun _ -> orders.Save order)
|> Flow.mapError OrderRejected
return order
}
let sendConfirmation (email: IEmailSender) (order: Order) : Flow<unit, PlaceOrderError, unit> =
flow {
do!
ColdTask(fun _ ->
task {
do! email.SendConfirmation order
return ()
})
}
let placeOrder
(orders: IOrderRepository)
(email: IEmailSender)
(order: Order)
: Flow<unit, PlaceOrderError, OrderId> =
flow {
let! validOrder = validateOrder order
let! savedOrder = saveOrder orders validOrder
do! sendConfirmation email savedOrder
return savedOrder.Id
}
validateOrder: Order -> Result<Order,PlaceOrderError>order: OrderFsLiveDocsGeneratedPage22_2D306DEB1823.OrderMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
FsLiveDocsGeneratedPage22_2D306DEB1823.PlaceOrderErrorSystem.StringRepresents text as a sequence of UTF-16 code units.
IsNullOrWhiteSpace: string -> boolIndicates whether a specified string is , empty, or consists only of white-space characters. The string to test. if the parameter is or , or if consists exclusively of white-space characters.
Email: stringErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
InvalidEmailOkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
saveOrder: IOrderRepository -> Order -> Flow<unit,PlaceOrderError,Order>orders: IOrderRepositoryFsLiveDocsGeneratedPage22_2D306DEB1823.IOrderRepositoryAxial.Flow`3unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
Axial.FlowfromTaskResult: (CancellationToken -> Task<Result<'value,'error>>) -> Flow<'env,'error,'value>Creates a flow from a cold task factory whose Error enters the typed error channel. The factory runs on each execution and receives the runtime cancellation token. Thrown exceptions are defects. .NET only
Save: Order -> Task<Result<unit,string>>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
mapError: ('error -> 'nextError) -> Flow<'env,'error,'value> -> Flow<'env,'nextError,'value>Maps the error value of a synchronous flow. Transforms the error type of the flow while leaving successful values untouched. Useful for mapping internal errors into public-facing domain errors. The function to transform the error value. The source flow. A with the transformed error type. let flow = Flow.fail "error" |> Flow.mapError (fun err -> err + "!")
OrderRejectedsendConfirmation: IEmailSender -> Order -> Flow<unit,PlaceOrderError,unit>email: IEmailSenderFsLiveDocsGeneratedPage22_2D306DEB1823.IEmailSenderColdTasktask: TaskBuilderBuilds a task using computation expression syntax.
SendConfirmation: Order -> TaskplaceOrder: IOrderRepository -> IEmailSender -> Order -> Flow<unit,PlaceOrderError,OrderId>FsLiveDocsGeneratedPage22_2D306DEB1823.OrderIdvalidOrder: OrdersavedOrder: OrderId: OrderIdRequires & types
Requires: open Axial; open System (packages: Axial)val validateOrder: Order -> Result<Order,PlaceOrderError>val saveOrder: IOrderRepository -> Order -> Flow<unit,PlaceOrderError,Order>val sendConfirmation: IEmailSender -> Order -> Flow<unit,PlaceOrderError,unit>val placeOrder: IOrderRepository -> IEmailSender -> Order -> Flow<unit,PlaceOrderError,OrderId>This approach keeps dependencies explicit:
- pure validation stays in
Result - each dependency is passed explicitly
Flowis only used where async work and typed execution outcomes matter
Flow.fromTaskResult and ColdTask are both cold task factories, but they bind different shapes. The repository
returns a Task<Result<_, _>>, so Flow.fromTaskResult sends its Error case to the typed-error channel (mapped with
Flow.mapError). The sender returns a bare Task, so ColdTask wraps it directly. Both discard the token here
(fun _ ->) because the interfaces do not accept one. When the underlying API can be cancelled, accept the token and
forward it, so cancelling the workflow stops the in-flight call:
type ICancellableSender =
abstract SendConfirmation : Order * CancellationToken -> Task
let sendConfirmationCancellable (email: ICancellableSender) (order: Order) : Flow<unit, PlaceOrderError, unit> =
flow {
do!
ColdTask(fun token ->
task {
do! email.SendConfirmation(order, token)
return ()
})
}
FsLiveDocsGeneratedPage22_2D306DEB1823.ICancellableSenderSendConfirmation: ICancellableSender -> Order * CancellationToken -> TaskFsLiveDocsGeneratedPage22_2D306DEB1823.OrderSystem.Threading.CancellationTokenPropagates notification that operations should be canceled.
System.Threading.Tasks.TaskRepresents an asynchronous operation.
sendConfirmationCancellable: ICancellableSender -> Order -> Flow<unit,PlaceOrderError,unit>email: ICancellableSenderorder: OrderAxial.Flow`3unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
FsLiveDocsGeneratedPage22_2D306DEB1823.PlaceOrderErrorflow: FlowBuilderThe universal flow { } computation expression.
ColdTasktoken: CancellationTokentask: TaskBuilderBuilds a task using computation expression syntax.
SendConfirmation: Order * CancellationToken -> TaskRequires & types
Requires: open Axial; open System.Threading; open System.Threading.Tasks (packages: Axial)val sendConfirmationCancellable: ICancellableSender -> Order -> Flow<unit,PlaceOrderError,unit>3. Realistic Implementations
type SqlOrderRepository() =
interface IOrderRepository with
member _.Save order =
task {
// In an application, this could use DbConnection, EF Core, or Dapper.
printfn "Saving %A to the database" order.Id
return Ok ()
}
type SmtpEmailSender() =
interface IEmailSender with
member _.SendConfirmation order =
task {
// In an application, this could use an SMTP client or SendGrid SDK.
printfn "Sending order email to %s" order.Email
}
FsLiveDocsGeneratedPage22_2D306DEB1823.SqlOrderRepositoryFsLiveDocsGeneratedPage22_2D306DEB1823.IOrderRepository_: SqlOrderRepositorySave: SqlOrderRepository -> Order -> Task<Result<unit,string>>order: Ordertask: TaskBuilderBuilds a task using computation expression syntax.
printfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
Id: OrderIdOkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
FsLiveDocsGeneratedPage22_2D306DEB1823.SmtpEmailSenderFsLiveDocsGeneratedPage22_2D306DEB1823.IEmailSender_: SmtpEmailSenderSendConfirmation: SmtpEmailSender -> Order -> TaskEmail: string4. Test Implementations
type RecordingOrderRepository(saved: ResizeArray<Order>) =
interface IOrderRepository with
member _.Save order =
task {
saved.Add order
return Ok ()
}
type RecordingEmailSender(sent: ResizeArray<string>) =
interface IEmailSender with
member _.SendConfirmation order =
task {
sent.Add order.Email
}
FsLiveDocsGeneratedPage22_2D306DEB1823.RecordingOrderRepositorysaved: ResizeArray<Order>ResizeArrayAn abbreviation for the CLI type
FsLiveDocsGeneratedPage22_2D306DEB1823.OrderFsLiveDocsGeneratedPage22_2D306DEB1823.IOrderRepository_: RecordingOrderRepositorySave: RecordingOrderRepository -> Order -> Task<Result<unit,string>>order: Ordertask: TaskBuilderBuilds a task using computation expression syntax.
Add: Order -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
FsLiveDocsGeneratedPage22_2D306DEB1823.RecordingEmailSendersent: ResizeArray<string>stringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage22_2D306DEB1823.IEmailSender_: RecordingEmailSenderSendConfirmation: RecordingEmailSender -> Order -> TaskAdd: string -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
Email: stringIf these doubles are awkward to use, reconsider the production dependency boundary.
5. Run The Flow
In production, placeOrder gets SqlOrderRepository() and SmtpEmailSender(). A test gives it the recording
doubles and checks both the outcome and what each dependency saw:
let saved = ResizeArray<Order>()
let sent = ResizeArray<string>()
let orders = RecordingOrderRepository(saved)
let email = RecordingEmailSender(sent)
let order =
{ Id = OrderId(Guid.Parse "7d3c2e1a-0000-4000-8000-000000000001")
Email = "ada@example.com"
Total = 99.95m }
placeOrder orders email order |> Flow.run () |> shouldEqual (Exit.Success order.Id)
List.ofSeq sent |> shouldEqual [ "ada@example.com" ]
placeOrder orders email { order with Email = " " } |> Flow.run () |> shouldEqual (Exit.Failure(Cause.Fail InvalidEmail))
saved.Count |> shouldEqual 1
saved: ResizeArray<Order>``.ctor``: unit -> unitInitializes a new instance of the class that is empty and has the default initial capacity.
FsLiveDocsGeneratedPage22_2D306DEB1823.Ordersent: ResizeArray<string>stringAn abbreviation for the CLI type . Basic Types
orders: RecordingOrderRepository``.ctor``: ResizeArray<Order> -> RecordingOrderRepositoryemail: RecordingEmailSender``.ctor``: ResizeArray<string> -> RecordingEmailSenderorder: OrderId: OrderIdOrderIdSystem.GuidRepresents a globally unique identifier (GUID).
Parse: string -> GuidConverts the string representation of a GUID to the equivalent structure. The string to convert. A structure that contains the value that was parsed. is . is not in a recognized format.
Email: stringTotal: decimalplaceOrder: IOrderRepository -> IEmailSender -> Order -> Flow<unit,PlaceOrderError,OrderId>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
ofSeq: 'T seq -> 'T listBuilds a new list from the given enumerable object. The input sequence. The list of elements from the sequence. let inputs = seq { 1; 2; 5 } inputs |> List.ofSeq Evaluates to [ 1; 2; 5 ]. This is an O(n) operation, where n is the length of the sequence.
FailureThe workflow failed due to a specific cause.
Axial.Cause`1Represents the cause of a failed workflow. The type of the domain-specific failure value.
FailAn expected domain-specific failure.
InvalidEmailCount: intGets the number of elements contained in the . The number of elements contained in the .
Requires & types
Requires: open Axial; open System (packages: Axial)val saved: ResizeArray<Order>val sent: ResizeArray<string>val orders: RecordingOrderRepositoryval email: RecordingEmailSenderval order: OrderThe invalid order failed validation before either dependency was called: one order was saved and one email sent.
At the application edge, turn the Exit into whatever the caller needs:
let describe (exit: Exit<OrderId, PlaceOrderError>) =
match exit with
| Exit.Success _ -> "placed"
| Exit.Failure(Cause.Fail InvalidEmail) -> "rejected before any dependency was called"
| Exit.Failure(Cause.Fail(OrderRejected reason)) -> $"the repository rejected the order: {reason}"
| Exit.Failure cause when Cause.isInterrupted cause -> "interrupted"
| Exit.Failure _ -> "unexpected failure"
describe: Exit<OrderId,PlaceOrderError> -> stringexit: Exit<OrderId,PlaceOrderError>Axial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
FsLiveDocsGeneratedPage22_2D306DEB1823.OrderIdFsLiveDocsGeneratedPage22_2D306DEB1823.PlaceOrderErrorSuccessThe workflow completed successfully.
FailureThe workflow failed due to a specific cause.
Axial.Cause`1Represents the cause of a failed workflow. The type of the domain-specific failure value.
FailAn expected domain-specific failure.
InvalidEmailOrderRejectedreason: stringcause: Cause<PlaceOrderError>Axial.CauseisInterrupted: Cause<'error> -> boolReturns whether the cause tree contains an interruption signal.
Requires & types
Requires: open Axial (packages: Axial)val describe: Exit<OrderId,PlaceOrderError> -> stringExit.Failure(Cause.Fail(OrderRejected "duplicate")) |> describe |> shouldEqual "the repository rejected the order: duplicate"
Axial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
FailureThe workflow failed due to a specific cause.
Axial.Cause`1Represents the cause of a failed workflow. The type of the domain-specific failure value.
FailAn expected domain-specific failure.
OrderRejected(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
describe: Exit<OrderId,PlaceOrderError> -> stringshouldEqual: 'a -> 'a -> unitRequires & types
Requires: open Axial (packages: Axial)6. Why This Stops Scaling
When several helpers pass the same dependencies through their parameters, use an environment record:
- the workflow code still depends on the same interfaces
- the execution boundary gets cleaner
- adding a third dependency becomes additive instead of rewriting every call site
Continue with Tutorial: App Record.

