Skip to content

Redis Example: Shopping Cart Service

End-to-end shopping cart with Redis storage, clustering, and reminders.

End-to-end guide — Redis storage, clustering, and reminders for a real-world shopping cart.

A shopping cart service where each cart is a durable Orleans grain backed by Redis. The service uses:

  • Redis clustering — silos discover each other through Redis instead of Kubernetes or Azure
  • Redis grain storage — cart state survives silo restarts
  • Redis reminders — an idle-cart cleanup reminder fires after 30 minutes of inactivity
  • Universal grain pattern — no C# interface stubs needed

Terminal window
docker run -d -p 6379:6379 redis:7-alpine
Terminal window
dotnet add package Orleans.FSharp
dotnet add package Orleans.FSharp.Runtime
dotnet add package Orleans.FSharp.Abstractions # C# shim for Orleans proxy gen
# Redis providers — optional at reference time, required at runtime
dotnet add package Microsoft.Orleans.Clustering.Redis
dotnet add package Microsoft.Orleans.Persistence.Redis
dotnet add package Microsoft.Orleans.Reminders.Redis

open Orleans.FSharp
type CartItem = { ProductId: string; Quantity: int; UnitPrice: decimal }
type CartState =
{ Items: CartItem list
CheckedOut: bool }
type CartCommand =
| AddItem of productId: string * qty: int * unitPrice: decimal
| RemoveItem of productId: string
| GetItems
| Checkout
| Clear

open Orleans.FSharp
open Orleans.FSharp.Runtime
let cartGrain =
grain {
defaultState { Items = []; CheckedOut = false }
persist "Default" // maps to the "Default" Redis storage provider
// Activate hook: log the cart creation
onActivate (fun state ->
task {
printfn "Cart grain activated"
return state
})
// Idle-cart cleanup: remind after 30 minutes of inactivity
onReminder "idle-cleanup" (fun state reminderName tickStatus ->
task {
// Deactivate empty or checked-out carts after inactivity
if state.Items.IsEmpty || state.CheckedOut then
printfn "Reminder '%s' fired — cart is empty or checked out, will deactivate" reminderName
return state
})
handle (fun state cmd -> task {
match cmd with
| AddItem(productId, qty, price) ->
if state.CheckedOut then
return state, box state // ignore — cart is closed
else
let existing = state.Items |> List.tryFind (fun i -> i.ProductId = productId)
let updated =
match existing with
| Some item ->
state.Items
|> List.map (fun i ->
if i.ProductId = productId
then { i with Quantity = i.Quantity + qty }
else i)
| None ->
{ ProductId = productId; Quantity = qty; UnitPrice = price }
:: state.Items
let newState = { state with Items = updated }
return newState, box newState
| RemoveItem productId ->
let newState =
{ state with Items = state.Items |> List.filter (fun i -> i.ProductId <> productId) }
return newState, box newState
| GetItems ->
return state, box state.Items
| Checkout ->
let newState = { state with CheckedOut = true }
return newState, box newState
| Clear ->
let newState = { Items = []; CheckedOut = false }
return newState, box newState
})
}

open System
open Orleans.FSharp.Runtime
let redisConn = Environment.GetEnvironmentVariable("REDIS_CONNECTION") // "localhost:6379"
let config = siloConfig {
addRedisClustering redisConn
addRedisStorage "Default" redisConn
addRedisReminderService redisConn
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}

The three Redis CE operations map to these Orleans packages:

CE operationNuGet package
addRedisClusteringMicrosoft.Orleans.Clustering.Redis
addRedisStorageMicrosoft.Orleans.Persistence.Redis
addRedisReminderServiceMicrosoft.Orleans.Reminders.Redis

open System
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Orleans.FSharp
open Orleans.FSharp.Runtime
[<EntryPoint>]
let main _ =
let redisConn =
Environment.GetEnvironmentVariable("REDIS_CONNECTION")
|> Option.ofObj
|> Option.defaultValue "localhost:6379"
let config = siloConfig {
addRedisClustering redisConn
addRedisStorage "Default" redisConn
addRedisReminderService redisConn
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}
let builder = Host.CreateApplicationBuilder()
SiloConfig.applyToHost config builder
// Register the grain definition
builder.Services.AddFSharpGrain<CartState, CartCommand>(cartGrain) |> ignore
let host = builder.Build()
host.Run()
0

A standalone client (separate process or service) connects to the cluster via static gateway clustering. The gateway port matches the silo’s default port (30000).

open System
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Orleans.FSharp
open Orleans.FSharp.Runtime
let clientCfg = clientConfig {
useStaticClustering ["127.0.0.1:30000"]
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}
let builder = HostApplicationBuilder()
ClientConfig.applyToHost clientCfg builder
let host = builder.Build()
host.Start()
let factory = host.Services.GetRequiredService<Orleans.IGrainFactory>()

For multi-silo production clusters, list multiple gateway addresses:

let clientCfg = clientConfig {
useStaticClustering ["10.0.0.1:30000"; "10.0.0.2:30000"; "10.0.0.3:30000"]
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}

open Orleans.FSharp
// Get a handle for user "user-42"'s cart
let cart = FSharpGrain.ref<CartState, CartCommand> factory "user-42"
// Add items
let! _ = cart |> FSharpGrain.send (AddItem("sku-001", 2, 9.99m))
let! _ = cart |> FSharpGrain.send (AddItem("sku-002", 1, 24.50m))
// Read the items (ask — result is CartItem list, not full CartState)
let! items = cart |> FSharpGrain.ask<CartState, CartCommand, CartItem list> GetItems
printfn "Cart has %d line(s)" items.Length
// Remove an item
do! cart |> FSharpGrain.post (RemoveItem "sku-001")
// Checkout
let! finalState = cart |> FSharpGrain.send Checkout
printfn "Checked out: %b" finalState.CheckedOut
FunctionWhen to use
FSharpGrain.ref factory keyGet a string-keyed handle
FSharpGrain.send cmd handleSend a command, get back the new state
FSharpGrain.post cmd handleSend a command, discard the result
FSharpGrain.ask<S,C,R> cmd handleSend a command, get back a specific result type (not the state)

Load connection strings from the environment

Section titled “Load connection strings from the environment”

Never inline connection strings in source code:

let redisConn =
Environment.GetEnvironmentVariable("REDIS_CONNECTION")
|> Option.ofObj
|> Option.defaultWith (fun () -> failwith "REDIS_CONNECTION is not set")

Or use IConfiguration:

open Microsoft.Extensions.Configuration
let redisConn = builder.Configuration.GetConnectionString("Redis")

Enable TLS between silos and clients using the useTls CE operation:

let config = siloConfig {
addRedisClustering redisConn
addRedisStorage "Default" redisConn
addRedisReminderService redisConn
useTls "my-cert-subject"
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}

For Redis itself, use a rediss:// connection string or pass TLS options through the underlying provider’s configuration.

let config = siloConfig {
addRedisClustering redisConn
addRedisStorage "Default" redisConn
addRedisReminderService redisConn
enableHealthChecks
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}

enableHealthChecks wires the Orleans liveness probe into ASP.NET Core’s /healthz endpoint (or your configured health check path).

Use a dedicated provider for frequently-written state to avoid sharing throughput:

let config = siloConfig {
addRedisClustering redisConn
addRedisStorage "CartStorage" redisConn // carts
addRedisStorage "SessionStore" redisConn // sessions on a different logical DB
addRedisReminderService redisConn
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}
// In the grain:
let cartGrain = grain {
persist "CartStorage"
// ...
}