Database & Cache Scaling
Altruist supports multi-instance Postgres and Redis configurations for scaling database reads, separating workloads, and adding redundancy.
Multi-Instance Postgres
Define multiple Postgres instances in config.yml — a primary for writes and one or more read replicas:
altruist:
persistence:
database:
provider: postgres
instances:
- name: primary
host: db-primary.example.com
port: 5432
username: gameserver
password: secret
database: mygame
role: readwrite
- name: replica
host: db-replica-1.example.com
port: 5432
username: gameserver_ro
password: secret
database: mygame
role: readonly
- name: analytics
host: db-analytics.example.com
port: 5432
username: analytics
password: secret
database: mygame_analytics
role: readwrite
Each instance is identified by its name. Use this name in [Vault] to route models to specific instances.
Routing Vault Models to Instances
Use DbInstance on the [Vault] attribute to target a specific database instance:
// Default: uses the first readwrite instance ("primary")
[Vault("players")]
public class PlayerVault : VaultModel { ... }
// Explicitly targets the read replica
[Vault("player_stats", DbInstance = "replica")]
public class PlayerStatsReadVault : VaultModel { ... }
// Targets the analytics database
[Vault("event_log", DbInstance = "analytics")]
public class EventLogVault : VaultModel { ... }
Note:
If DbInstance is empty (the default), the vault uses the first available ISqlDatabaseProvider. When instances are configured, this is the first instance in the list. When no instances are configured (single database), it uses the standard provider.
Instance Properties
| Property | Description | Default |
|---|---|---|
name | Unique identifier for this instance (used as service key) | Required |
host | Database hostname | localhost |
port | Database port | 5432 |
username | Connection username | Required |
password | Connection password | Required |
database | Database name | Required |
role | readwrite or readonly — informational, for your own routing logic | readwrite |
pooling | Enable connection pooling | true |
ssl-mode | SSL mode: disable, prefer, require, verify-ca, verify-full | disable |
Read/Write Split Pattern
A common pattern is to use the primary for writes and mutations, and a replica for heavy read queries:
[Service]
public class LeaderboardService
{
// Writes go to primary (default)
private readonly IAutosaveService<PlayerVault> _autosave;
// Heavy reads go to replica
private readonly IVault<LeaderboardReadVault> _leaderboard;
public LeaderboardService(
IAutosaveService<PlayerVault> autosave,
IVault<LeaderboardReadVault> leaderboard)
{
_autosave = autosave;
_leaderboard = leaderboard;
}
public void UpdateScore(PlayerVault player, int score)
{
player.Score = score;
_autosave.MarkDirty(player, player.StorageId);
// Writes to primary via autosave
}
public async Task<List<LeaderboardReadVault>> GetTopPlayers()
{
// Reads from replica — doesn't load the primary
return await _leaderboard
.OrderByDescending(p => p.Score)
.Take(100)
.ToListAsync();
}
}
[Vault("players", DbInstance = "replica")]
public class LeaderboardReadVault : VaultModel
{
[VaultColumn] public override string StorageId { get; set; } = "";
[VaultColumn] public string Name { get; set; } = "";
[VaultColumn] public int Score { get; set; }
}
Note:
Read replicas handle replication lag at the database level (typically milliseconds to low seconds). Leaderboard queries, statistics, and analytics are ideal candidates — they tolerate slightly stale data and the read load doesn't impact write performance on the primary.
Single Database (No Instances)
If you don't need multiple instances, use the simple config — no instances section needed:
altruist:
persistence:
database:
provider: postgres
host: localhost
port: 5432
username: gameserver
password: secret
database: mygame
Note:
The single-database config and the multi-instance config are mutually compatible. The framework detects which style you're using. You can start with a single database and add instances later without changing any vault model code (unless you want to route specific models to specific instances).
Redis Cache
Altruist uses Redis as a shared cache layer — for autosave dirty tracking, session data, and cross-channel communication.
Single Redis Instance
altruist:
persistence:
cache:
provider: redis
redis:
connection-string: "localhost:6379"
Redis with Sentinel (High Availability)
StackExchange.Redis natively supports Sentinel for automatic failover:
altruist:
persistence:
cache:
provider: redis
redis:
connection-string: "sentinel1:26379,sentinel2:26379,sentinel3:26379,serviceName=mymaster"
Note:
With Sentinel, if the master goes down, a replica is automatically promoted. The game server reconnects transparently — no code changes needed. Altruist's RedisConnectionFactory uses AbortOnConnectFail = false with infinite retry, so it handles transient connection loss gracefully.
Redis Cluster
For horizontal scaling across multiple Redis nodes:
altruist:
persistence:
cache:
provider: redis
redis:
connection-string: "node1:6379,node2:6379,node3:6379"
Keys are automatically distributed across nodes via hash slots. Each node owns a range of the 16384 hash slots.
Note:
Redis Cluster requires that all keys accessed in a single operation belong to the same hash slot. Altruist's cache operations work per-entity (single key), so this is not an issue for normal autosave and session usage.
Full Production Architecture
A production game server typically combines multi-channel servers, read replicas, and Redis:
altruist:
persistence:
cache:
provider: redis
redis:
connection-string: "redis-primary:6379,redis-replica:6380"
database:
provider: postgres
instances:
- name: primary
host: pg-primary
port: 5432
username: gameserver
password: "${DB_PASSWORD}"
database: mygame
role: readwrite
ssl-mode: require
- name: replica
host: pg-replica
port: 5432
username: gameserver_ro
password: "${DB_PASSWORD}"
database: mygame
role: readonly
ssl-mode: require
Note:
Environment variables work in config values (e.g., ${DB_PASSWORD}). Combined with the ALTRUIST__ prefix for env var overrides, you can keep secrets out of config files entirely. See Multi-Channel Servers for Docker Compose examples.