Build a leaderboard with atomic counters
In this tutorial, you build a shared leaderboard where each player has a score that many clients can update at the same time without losing an update. You use atomic counters so that concurrent increments always add up correctly, then read the scores back and rank them. You need the .NET SDK.
Every counter update is applied atomically on the server, so you never read a value into your process, add to it, and write it back — that pattern loses updates under concurrency, and counters exist to avoid it. Counters live on IZaris, so you cast the client to IZaris first.
1. Create a project and add the SDK
Create a console project and add the Zaris SDK.
dotnet new console -o zaris-leaderboard
cd zaris-leaderboard
dotnet add package Clustron.Zaris.SDK
2. Get an IZaris client
In Program.cs, register an in-process store and resolve the client as IZaris so the Counters accessor is available.
using Clustron.Zaris.Client;
using Clustron.Zaris.Client.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection()
.AddClustronZaris("demo", "zaris://inproc/demo")
.BuildServiceProvider();
var client = (IZaris)await services
.GetRequiredService<IZarisClientProvider>()
.GetAsync("demo");
3. Award points with AddAsync
Use Counters.AddAsync to add points to a player's score. Give each player their own counter key, such as score:alice. AddAsync returns a KvResult<CounterValue>; the running total after the update is on Value.Current.
var result = await client.Counters.AddAsync("score:alice", 10);
if (result.IsSuccess)
{
Console.WriteLine($"Alice's score is now {result.Value.Current}");
}
The first AddAsync for a key starts the counter from zero and applies the delta, so you do not create the counter separately. Pass a negative delta to subtract points.
4. Simulate concurrent scoring
Because each update is atomic, concurrent increments never collide. Fire many increments in parallel and the total is exact — no lost updates, no lock, no retry loop.
var tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
tasks.Add(client.Counters.AddAsync("score:alice", 1));
}
await Task.WhenAll(tasks);
After the 10 points from the previous step and these 100 increments, Alice's score is exactly 110.
5. Read a score with GetAsync
Use Counters.GetAsync to read a player's current score. It returns a KvResult<long>, so the value is a plain long on Value. Check IsSuccess before reading it.
var alice = await client.Counters.GetAsync("score:alice");
if (alice.IsSuccess)
{
Console.WriteLine($"Alice: {alice.Value}");
}
6. Build and rank the leaderboard
Give a few players scores, read each counter, then sort in your process to produce the ranking. This example seeds three players and prints them highest-first.
var players = new[] { "alice", "bob", "carol" };
await client.Counters.AddAsync("score:bob", 75);
await client.Counters.AddAsync("score:carol", 95);
var board = new List<(string Player, long Score)>();
foreach (var player in players)
{
var score = await client.Counters.GetAsync($"score:{player}");
if (score.IsSuccess)
{
board.Add((player, score.Value));
}
}
foreach (var entry in board.OrderByDescending(e => e.Score))
{
Console.WriteLine($"{entry.Player}: {entry.Score}");
}
Running this after the earlier steps prints the players ranked by score, with Alice on top at 110.
What you built
You built a leaderboard whose scores stay correct no matter how many clients update them at once. The key idea is that AddAsync applies each change atomically on the server, so you increment through it rather than reading and writing back — that is what keeps 100 concurrent increments from losing any. You read the running total from Value.Current on an add and from Value on a GetAsync, kept one counter per player, and ranked them by reading the known set of keys. Reserve SetAsync for seeding or resetting a counter to a known value, never for incrementing.