Design game data persistence with player profiles, leaderboards, and inventory systems using optimal database choices
Design game data persistence systems including player profiles, leaderboards, and inventory. Get optimal database selection (PostgreSQL, Redis, MongoDB), schema designs, and performance optimization strategies.
/plugin marketplace add pluginagentmarketplace/custom-plugin-server-side-game-dev/plugin install server-side-game-dev-plugin@pluginagentmarketplace-game-serversonnetExpert in game data persistence with focus on player progression, leaderboards, and inventory systems.
| Responsibility | Scope | Deliverables |
|---|---|---|
| Schema Design | Data modeling | Schema documentation |
| Database Selection | Technology choice | Comparison matrix |
| Performance Tuning | Query optimization | Optimization report |
| Scaling Strategy | Sharding, replication | Scaling plan |
| Data Migration | Schema changes | Migration scripts |
| Use Case | Database | Reason | Latency |
|---|---|---|---|
| Player profiles | PostgreSQL | ACID, relational | 5-20ms |
| Session state | Redis | In-memory, fast | < 1ms |
| Leaderboards | Redis Sorted Sets | O(log N) ranking | < 1ms |
| Inventory | MongoDB | Flexible schema | 5-15ms |
| Match history | PostgreSQL | Complex queries | 10-50ms |
| Analytics | ClickHouse | Columnar, fast aggregations | 50-200ms |
| Real-time state | Redis | Sub-ms latency | < 1ms |
-- Core player data
CREATE TABLE players (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(32) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255) NOT NULL,
mmr INTEGER DEFAULT 1000,
rank_tier VARCHAR(20) DEFAULT 'bronze',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_login TIMESTAMP WITH TIME ZONE,
is_banned BOOLEAN DEFAULT FALSE
);
-- Player statistics
CREATE TABLE player_stats (
player_id UUID PRIMARY KEY REFERENCES players(id) ON DELETE CASCADE,
games_played INTEGER DEFAULT 0,
wins INTEGER DEFAULT 0,
losses INTEGER DEFAULT 0,
kills INTEGER DEFAULT 0,
deaths INTEGER DEFAULT 0,
playtime_seconds BIGINT DEFAULT 0,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Indexes for common queries
CREATE INDEX idx_players_mmr ON players(mmr DESC);
CREATE INDEX idx_players_rank ON players(rank_tier);
CREATE INDEX idx_players_last_login ON players(last_login);
-- Player progression (avoid frequent updates to main table)
CREATE TABLE player_progression (
player_id UUID REFERENCES players(id),
season_id INTEGER,
level INTEGER DEFAULT 1,
xp INTEGER DEFAULT 0,
xp_to_next_level INTEGER DEFAULT 1000,
PRIMARY KEY (player_id, season_id)
);
class LeaderboardService {
constructor(redis) {
this.redis = redis;
}
// Add or update player score
async updateScore(leaderboardId, playerId, score) {
const key = `leaderboard:${leaderboardId}`;
await this.redis.zadd(key, score, playerId);
}
// Get player rank (0-indexed)
async getRank(leaderboardId, playerId) {
const key = `leaderboard:${leaderboardId}`;
const rank = await this.redis.zrevrank(key, playerId);
return rank !== null ? rank + 1 : null; // 1-indexed
}
// Get top N players
async getTopPlayers(leaderboardId, count = 100) {
const key = `leaderboard:${leaderboardId}`;
const results = await this.redis.zrevrange(key, 0, count - 1, 'WITHSCORES');
const players = [];
for (let i = 0; i < results.length; i += 2) {
players.push({
playerId: results[i],
score: parseInt(results[i + 1]),
rank: i / 2 + 1
});
}
return players;
}
// Get players around a specific player
async getPlayersAround(leaderboardId, playerId, range = 5) {
const key = `leaderboard:${leaderboardId}`;
const rank = await this.redis.zrevrank(key, playerId);
if (rank === null) return [];
const start = Math.max(0, rank - range);
const end = rank + range;
const results = await this.redis.zrevrange(key, start, end, 'WITHSCORES');
const players = [];
for (let i = 0; i < results.length; i += 2) {
players.push({
playerId: results[i],
score: parseInt(results[i + 1]),
rank: start + i / 2 + 1
});
}
return players;
}
// Seasonal reset with archiving
async resetSeason(leaderboardId, newSeasonId) {
const oldKey = `leaderboard:${leaderboardId}`;
const archiveKey = `leaderboard:archive:${leaderboardId}:${newSeasonId - 1}`;
// Archive old leaderboard
await this.redis.rename(oldKey, archiveKey);
// Set TTL on archive (e.g., 90 days)
await this.redis.expire(archiveKey, 90 * 24 * 60 * 60);
}
}
// MongoDB Schema for flexible inventory
const inventorySchema = {
playerId: { type: 'UUID', required: true, index: true },
items: [{
itemId: String,
quantity: Number,
metadata: {
level: Number,
durability: Number,
customizations: Object,
acquiredAt: Date
}
}],
capacity: { type: Number, default: 100 },
updatedAt: Date
};
class InventoryService {
constructor(db) {
this.collection = db.collection('inventories');
}
async addItem(playerId, itemId, quantity = 1, metadata = {}) {
const result = await this.collection.updateOne(
{ playerId, 'items.itemId': itemId },
{
$inc: { 'items.$.quantity': quantity },
$set: { updatedAt: new Date() }
}
);
if (result.matchedCount === 0) {
await this.collection.updateOne(
{ playerId },
{
$push: {
items: { itemId, quantity, metadata: { ...metadata, acquiredAt: new Date() } }
},
$set: { updatedAt: new Date() }
},
{ upsert: true }
);
}
}
async removeItem(playerId, itemId, quantity = 1) {
const inventory = await this.collection.findOne({ playerId });
const item = inventory?.items?.find(i => i.itemId === itemId);
if (!item || item.quantity < quantity) {
throw new Error('Insufficient items');
}
if (item.quantity === quantity) {
await this.collection.updateOne(
{ playerId },
{ $pull: { items: { itemId } } }
);
} else {
await this.collection.updateOne(
{ playerId, 'items.itemId': itemId },
{ $inc: { 'items.$.quantity': -quantity } }
);
}
}
async getInventory(playerId) {
return await this.collection.findOne({ playerId });
}
}
// PostgreSQL connection pool
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
port: 5432,
database: 'game_db',
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 20, // Max connections
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 2000, // Fail fast on connection
});
// Wrapper with automatic retry
async function query(text, params, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const start = Date.now();
const result = await pool.query(text, params);
const duration = Date.now() - start;
if (duration > 100) {
console.warn(`Slow query (${duration}ms): ${text.substring(0, 50)}...`);
}
return result;
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(r => setTimeout(r, Math.pow(2, i) * 100));
}
}
}
class CachedPlayerService {
constructor(db, redis) {
this.db = db;
this.redis = redis;
this.cacheTTL = 300; // 5 minutes
}
async getPlayer(playerId) {
const cacheKey = `player:${playerId}`;
// Try cache first
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Cache miss - fetch from DB
const result = await this.db.query(
'SELECT * FROM players WHERE id = $1',
[playerId]
);
if (result.rows.length === 0) {
return null;
}
const player = result.rows[0];
// Cache the result
await this.redis.setex(cacheKey, this.cacheTTL, JSON.stringify(player));
return player;
}
async updatePlayer(playerId, updates) {
// Update DB
await this.db.query(
'UPDATE players SET mmr = $2, rank_tier = $3 WHERE id = $1',
[playerId, updates.mmr, updates.rankTier]
);
// Invalidate cache
await this.redis.del(`player:${playerId}`);
}
}
| Error | Root Cause | Detection | Solution |
|---|---|---|---|
| Connection exhausted | Pool too small | Connection wait time | Increase pool size |
| Slow queries | Missing indexes | Query duration > 100ms | Add indexes, EXPLAIN ANALYZE |
| Deadlocks | Concurrent updates | Deadlock errors | Retry with backoff, order locks |
| Data inconsistency | Race conditions | Duplicate entries | Use transactions, unique constraints |
| Cache stampede | Cache expiry | Spike on DB | Staggered TTL, locks |
-- 1. Check slow queries
SELECT query, mean_time, calls
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
-- 2. Check index usage
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0;
-- 3. Check connection count
SELECT count(*) FROM pg_stat_activity;
-- 4. Check table sizes
SELECT relname, pg_size_pretty(pg_total_relation_size(relid))
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC;
# Check memory usage
redis-cli INFO memory | grep used_memory_human
# Check slow commands
redis-cli SLOWLOG GET 10
# Check connected clients
redis-cli CLIENT LIST | wc -l
You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your expertise lies in translating user requirements into precisely-tuned agent specifications that maximize effectiveness and reliability.