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
Shared setup
// Setup for the checked examples on this page.
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.JavaScript
/// 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 actual
SystemIOThreadingTasksAxialLayersConsoleFileSystemHostingBrowserNodePlatformServiceStateTelemetryJavaScriptshouldEqual: 'a -> 'a -> unitexpected: 'aactual: 'a(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
open System
open System.Threading.Tasks
type OrderId = OrderId of Guid
type Order =
{ Id: OrderId
Email: string
Total: decimal }
type PlaceOrderError =
| InvalidEmail
| OrderRejected of string
| TimedOut
| Cancelled
type IOrderRepository =
abstract Save : Order -> Task<Result<unit, string>>
type IEmailSender =
abstract SendConfirmation : Order -> Task
SystemThreadingTasksFsLiveDocsGeneratedPage19_4556FC416C2B.OrderIdOrderIdSystem.GuidRepresents a globally unique identifier (GUID).
FsLiveDocsGeneratedPage19_4556FC416C2B.OrderId: OrderIdEmail: stringstringAn abbreviation for the CLI type . Basic Types
Total: decimaldecimalAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage19_4556FC416C2B.PlaceOrderErrorInvalidEmailOrderRejectedTimedOutCancelledFsLiveDocsGeneratedPage19_4556FC416C2B.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
FsLiveDocsGeneratedPage19_4556FC416C2B.IEmailSenderSendConfirmation: IEmailSender -> Order -> TaskSystem.Threading.Tasks.TaskRepresents an asynchronous operation.
These interfaces are intentionally narrow. They represent what the workflow needs, not the full database or mail client API.
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: OrderFsLiveDocsGeneratedPage19_4556FC416C2B.OrderMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
FsLiveDocsGeneratedPage19_4556FC416C2B.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: IOrderRepositoryFsLiveDocsGeneratedPage19_4556FC416C2B.IOrderRepositoryAxial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
unitThe 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: IEmailSenderFsLiveDocsGeneratedPage19_4556FC416C2B.IEmailSenderColdTasktask: TaskBuilderBuilds a task using computation expression syntax.
SendConfirmation: Order -> TaskplaceOrder: IOrderRepository -> IEmailSender -> Order -> Flow<unit,PlaceOrderError,OrderId>FsLiveDocsGeneratedPage19_4556FC416C2B.OrderIdvalidOrder: OrdersavedOrder: OrderId: OrderIdNothing is hidden here:
- pure validation stays in
Result - each dependency is passed explicitly
Flowis only used where async work and typed execution outcomes matter
3. Realistic Implementations
type SqlOrderRepository() =
interface IOrderRepository with
member _.Save order =
task {
// Imagine the real dependency here: DbConnection, EF Core, Dapper, etc.
printfn "Saving %A to the database" order.Id
return Ok ()
}
type SmtpEmailSender() =
interface IEmailSender with
member _.SendConfirmation order =
task {
// Imagine the real dependency here: SMTP client, SendGrid SDK, etc.
printfn "Sending order email to %s" order.Email
}
FsLiveDocsGeneratedPage19_4556FC416C2B.SqlOrderRepositoryFsLiveDocsGeneratedPage19_4556FC416C2B.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.
FsLiveDocsGeneratedPage19_4556FC416C2B.SmtpEmailSenderFsLiveDocsGeneratedPage19_4556FC416C2B.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
}
FsLiveDocsGeneratedPage19_4556FC416C2B.RecordingOrderRepositorysaved: ResizeArray<Order>ResizeArrayAn abbreviation for the CLI type
FsLiveDocsGeneratedPage19_4556FC416C2B.OrderFsLiveDocsGeneratedPage19_4556FC416C2B.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.
FsLiveDocsGeneratedPage19_4556FC416C2B.RecordingEmailSendersent: ResizeArray<string>stringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage19_4556FC416C2B.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: stringThese test doubles are boring on purpose. If this shape is awkward to test, the production dependency boundary is not sharp enough yet.
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.
FsLiveDocsGeneratedPage19_4556FC416C2B.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 .
The 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.
FsLiveDocsGeneratedPage19_4556FC416C2B.OrderIdFsLiveDocsGeneratedPage19_4556FC416C2B.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.
Exit.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 -> unit6. Why This Stops Scaling
Passing two dependencies explicitly is fine. Passing five through every helper is not.
That is the point where you move to 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.

