Skip to content

How to Build Distributed Systems with F# and Orleans

Step-by-step guide to building distributed systems with Orleans.FSharp — from installation to production deployment with Microsoft Orleans and F#

Build a distributed system with F# and Microsoft Orleans in under 15 minutes.

Orleans.FSharp provides idiomatic F# computation expressions for Microsoft Orleans, the virtual actor framework. This guide walks you through the entire process — from installing the .NET SDK to running a production-ready silo with grains, state persistence, and property-based tests.

  • .NET 10 SDK or later
  • A code editor (VS Code with Ionide, JetBrains Rider, or Visual Studio)

Orleans.FSharp ships a dotnet new template that scaffolds a complete solution:

Terminal window
dotnet new install Orleans.FSharp.Templates

Generate a working Orleans.FSharp solution with a silo, grain definitions, and tests:

Terminal window
dotnet new orleans-fsharp -n MyDistributedApp
cd MyDistributedApp

This creates:

  • src/MyDistributedApp.Silo/ — the host process with silo configuration
  • src/MyDistributedApp.Grains/ — grain definitions using grain {} CEs
  • tests/MyDistributedApp.Tests/ — FsCheck property tests with GrainArbitrary

Step 3: Define a grain with discriminated union state

Section titled “Step 3: Define a grain with discriminated union state”

Open the grains project and define your state as an F# discriminated union:

open Orleans
open Orleans.FSharp
[<GenerateSerializer>]
type AccountState =
| [<Id(0u)>] Inactive
| [<Id(1u)>] Active of balance: decimal
[<GenerateSerializer>]
type AccountCommand =
| [<Id(0u)>] Deposit of decimal
| [<Id(1u)>] Withdraw of decimal
| [<Id(2u)>] GetBalance
| [<Id(3u)>] Close

Step 4: Implement the grain with the grain {} computation expression

Section titled “Step 4: Implement the grain with the grain {} computation expression”

Use the grain {} CE to define the grain declaratively — no class inheritance, no mutable state:

let account =
grain {
defaultState Inactive
handle (fun state cmd ->
task {
match state, cmd with
| Inactive, Deposit amount when amount > 0m ->
return Active amount, box amount
| Active balance, Deposit amount when amount > 0m ->
let newBalance = balance + amount
return Active newBalance, box newBalance
| Active balance, Withdraw amount when amount > 0m && amount <= balance ->
let newBalance = balance - amount
if newBalance = 0m then
return Inactive, box 0m
else
return Active newBalance, box newBalance
| Active balance, GetBalance ->
return Active balance, box balance
| Inactive, GetBalance ->
return Inactive, box 0m
| Active _, Close ->
return Inactive, box true
| _ ->
return state, box false
})
persist "Default"
}

The F# compiler ensures every state-command combination is handled. Invalid transitions are caught at compile time, not runtime.

Use the siloConfig {} CE to configure Microsoft Orleans clustering, storage, and streaming:

open Orleans.FSharp.Runtime
let config = siloConfig {
useLocalhostClustering // single-node for development
addMemoryStorage "Default" // in-memory state (swap to Redis/Azure for production)
enableDashboard 8080 // Orleans Dashboard at http://localhost:8080
}

For production, replace with persistent providers:

let prodConfig = siloConfig {
useRedisClustering "redis-connection-string"
addRedisStorage "Default" "redis-connection-string"
addMemoryStreams "StreamProvider"
enableHealthChecks
}
Terminal window
dotnet build
dotnet test
dotnet run --project src/MyDistributedApp.Silo

The silo starts, activates grains on demand, and persists state automatically. Grains are virtual actors — they are always addressable and activated on first call.

Orleans.FSharp includes GrainArbitrary for FsCheck, which auto-generates random command sequences from your DU definition:

open FsCheck
open FsCheck.Xunit
open Orleans.FSharp.Testing
let accountInvariant state =
match state with
| Inactive -> true
| Active balance -> balance > 0m
let applyCommand state cmd =
match state, cmd with
| Inactive, Deposit amount when amount > 0m -> Active amount
| Active balance, Deposit amount when amount > 0m -> Active(balance + amount)
| Active balance, Withdraw amount when amount > 0m && amount <= balance ->
if balance - amount = 0m then Inactive else Active(balance - amount)
| _ -> state
[<Property>]
let ``account balance is never negative`` () =
let arb = GrainArbitrary.forCommands<AccountCommand>()
Prop.forAll arb (fun commands ->
FsCheckHelpers.stateMachineProperty Inactive applyCommand accountInvariant commands)

Publish and subscribe to event streams with typed StreamRef<'T>:

open Orleans.FSharp.Streaming
// In a grain handler:
let! streamRef = Stream.getRef<AccountEvent> ctx "StreamProvider" "Accounts" grainId
do! Stream.publish streamRef (Deposited amount)

Orleans.FSharp supports all Microsoft Orleans production features:

  • Clustering: Redis, Azure Table Storage, Consul, ZooKeeper, Kubernetes
  • State persistence: Redis, Azure Blob, Cosmos DB, DynamoDB, ADO.NET (SQL Server, PostgreSQL)
  • Streaming: Event Hubs, Azure Queue, memory streams
  • Security: TLS/mTLS, call filters, request context propagation
  • Observability: OpenTelemetry, health checks, Orleans Dashboard

See the Silo Configuration and Security guides for production setup.