Ledger

Overview

A lock free datastore library for Roblox, where state is a fold over a log of changes.

Ledger keeps player data as a log of changes instead of a document you overwrite. You never write state. You write down the change you want, a function you own decides if it's allowed, and state is what falls out of replaying those changes.

local Store = Ledger.New({
	Name = "PlayerData",
	Default = { Gold = 100 },
	Reducer = function(State, Op)
		if Op.Kind == "Earn" then
			if type(Op.Amount) ~= "number" or Op.Amount <= 0 then
				return nil
			end
			return { Gold = State.Gold + Op.Amount }
		end

		if Op.Kind == "SpendGold" then
			if type(Op.Amount) ~= "number" or Op.Amount > State.Gold then
				return nil -- refused, on every server, forever
			end
			return { Gold = State.Gold - Op.Amount }
		end

		return nil -- an op this build has never heard of
	end,
})

Store:Load(Player)
Store:Expect(Player):Apply("SpendGold", { Amount = 25 })

Two servers spend the same 100 gold, both writes go through, the fold takes one and refuses the other. Every server agrees, every time.

Why there's no session lock

A session lock stops the second writer. You pay for that in three ways. A dead server leaves a lease someone has to wait out. Whoever is locked waits at the join screen. And nothing can reach a player who is offline or on another server.

A fold that validates makes the bad state unreachable, so nothing needs unlocking when a server crashes. It costs discipline instead. Changes have to be ops with names, and you have to write a reducer.

What's in it

What it isn't

Ledger is server side only. It doesn't replicate to clients, it doesn't do leaderboards or ordered stores, and it won't hide a reducer that's wrong. If your reducer reads os.time() or math.random(), two servers will fold the same log into different state. Ledger warns about that in Studio rather than hiding it.

Ledger is tested the way TigerBeetle tests a database. It runs against a simulated datastore that drops writes, reports success for writes it dropped, corrupts records, splits servers apart, kills them halfway through a write, and moves the clock. Everything comes off one seed, so a failure replays exactly. The transaction protocol also goes through an exhaustive interleaving search. See Testing.

On this page