Code Review: Simple and Flexible DataManager Module

Hello everyone,

I recently developed a DataManager Module for Roblox that is designed to load and save player data. It’s kept simple but is also flexible and reusable for different data stores (e.g., for Leaderstats or Items). It uses UpdateAsync to efficiently update player data.



Here is the ModuleScript: (80 Lines)

local DataStoreService = game:GetService("DataStoreService")
local httpService = game:GetService("HttpService")

local DataManager = {}


-- //: If plr isn't valid: false.
-- //: If player is valid: true.
local function checkPlayer(plr: Player)
	if not plr or not plr:IsA("Player") then
		warn("Invalid Player as argument, got: "..tostring(typeof(plr)))
		return false
	end
	return true
end


-- //: Function for Loading Data
DataManager.LoadData = function(plr: Player, dataStoreName: string)
	if not checkPlayer(plr) then return end -- Check if player argument is valid
	
	-- Get the DataStore and the key:
	local dataStore = DataStoreService:GetDataStore(dataStoreName)
	local key = plr.UserId
	
	-- Try to get the Data for the Player:
	local success, returnValue = pcall(function()
		return dataStore:GetAsync(key)
	end)
	
	if success then
		-- Check if loaded data is nil or not:
		if returnValue ~= nil then
			print(plr.Name .. " Successfully Loaded this Data: " .. httpService:JSONEncode(returnValue))
			return returnValue
		else
			warn("No Data was found for Player: " .. plr.Name .. " (Key: " .. plr.UserId .. ")")
			return nil
		end
		
	else
		warn(plr.Name..": Error while Loading Data:", returnValue)
		return nil
	end
end


-- //: Function for Changing the players Data to something new
DataManager.ChangeData = function(plr: Player, dataStoreName: string, indexToChange: any, newValue: any)
	if not checkPlayer(plr) then return end -- Check if player argument is valid

	-- Get the DataStore and the key:
	local dataStore = DataStoreService:GetDataStore(dataStoreName)
	local key = plr.UserId
	
	-- If the key is empty, just create new table and assign value
	-- If key is not empty, just add the value to the table
	local success, err = pcall(function()
		
		dataStore:UpdateAsync(key, function(data)
			if data ~= nil then
				data[indexToChange] = newValue
			else
				data = { [indexToChange] = newValue }
			end
			return data -- Save the Changes
		end)
		
	end)
	
	if success then
		print("Successfully Changed Data for: "..plr.Name.." (Key: "..key..")")
	else
		warn("Error while Chaninging Data for: "..plr.Name.." (Key: "..key.."):", err)
	end
end



return DataManager

The module provides two main functions:

  • LoadData: Loads the player’s saved data from the specified DataStore.

  • ChangeData: Changes specific values in the key of the player.



Example usage with Leaderstats, ServerScript: (29 Lines)

local dataManager = require(game:GetService("ServerScriptService").Modules.DataManager)


game.Players.PlayerAdded:Connect(function(plr)
	
	-- Leaderstats:
	local leaderstats = Instance.new("Folder", plr)
	leaderstats.Name = "leaderstats"
	
	local money = Instance.new("NumberValue", leaderstats)
	money.Name = "Money"
	
	local level = Instance.new("NumberValue", leaderstats)
	level.Name = "Level"
	
	
	-- Load Data:
	local loadedData = dataManager.LoadData(plr, "PlayerData")
	if not loadedData then warn("No Data") return end
	
	plr.leaderstats.Money.Value = loadedData["Money"]
	plr.leaderstats.Level.Value = loadedData["Level"]
end)


game.Players.PlayerRemoving:Connect(function(plr)
	dataManager.ChangeData(plr, "PlayerData", "Money", plr.leaderstats.Money.Value)
	dataManager.ChangeData(plr, "PlayerData", "Level", plr.leaderstats.Level.Value)
end)

I tested it and it works


I’m looking for feedback and suggestions for improvement, especially regarding efficiency and best practices. Are there any optimizations I might have missed? Or is there something I should pay more attention to?

Thanks for your time and help!

I have a few comments and suggestions.

  1. I like the way you are documenting everything, although a lot of it is redundant due to the name of the functions or variables, since they are descriptive enough to understand what is going on.
  2. For LoadData and ChangeData I would suggest an approach that incorporates retries, and some other means to ensure that the data has properly been retrieved or set.
  3. I strongly suggest caching your LoadData and having ChangeData save the data in one go instead of having to call UpdateAsync every time you want to change a single property. You could instead just update the entire table.
  4. Critique on your example: For ChangeData, you will also want to call it periodically and when the game closes.