Skip to main content

Read Replication

Hysteria ORM provides built-in support for database read replication, allowing you to scale your read operations across multiple slave databases while ensuring all write operations go to the master. This is particularly useful for high-traffic applications that have more read operations than writes.

Overview

Read replication in Hysteria ORM works by:

  • Automatically routing read operations (SELECT queries) to slave databases
  • Routing all write operations (INSERT, UPDATE, DELETE) to the master database
  • Supporting multiple load-balancing algorithms (Round Robin and Random)
  • Providing explicit control over which database to use when needed

Basic Configuration

Configure slaves when creating your SqlDataSource:

import { SqlDataSource } from "hysteria-orm";
import { User } from "./models/user";

const sql = new SqlDataSource({
type: "postgres",
host: "master.db.com",
username: "root",
password: "password",
database: "mydb",

// Configure slave databases
replication: {
slaves: [
{
type: "postgres",
host: "slave1.db.com",
username: "root",
password: "password",
database: "mydb",
},
{
type: "postgres",
host: "slave2.db.com",
username: "root",
password: "password",
database: "mydb",
},
],
slaveAlgorithm: "roundRobin",
},
});

await sql.connect();

await sql.connect();

Slave Selection Algorithms

Round Robin (Default)

Distributes requests evenly across all slaves in sequence. Each slave gets an equal share of the traffic.

const sql = new SqlDataSource({
// ... connection config
replication: {
slaves: [
// ... slave configurations
],
slaveAlgorithm: "roundRobin",
},
});

Use case: Best for evenly distributed load when all slaves have similar capacity.

Random

Randomly selects a slave for each request.

const sql = new SqlDataSource({
// ... connection config
replication: {
slaves: [
// ... slave configurations
],
slaveAlgorithm: "random",
},
});

Use case: Useful for simple load distribution without tracking state.

Automatic Routing

By default, Hysteria ORM automatically routes operations to the appropriate database:

// Read operations → Automatically uses slaves or master as fallback
const users = await User.find();
const user = await User.findOne({ where: { id: 1 } });
const count = await User.query().getCount();
const paginated = await User.query().paginate(1, 10);

// Write operations → Always uses master
const newUser = await User.insert({ name: "John", email: "john@example.com" });
await User.query().where("id", 1).update({ name: "Jane" });
await User.query().where("id", 1).delete();

Explicit Replication Mode

You can override the automatic behavior using the replicationMode option:

Force Master for Reads

Useful when you need strong consistency immediately after a write:

// Write to master
await User.insert({ name: "John", email: "john@example.com" });

// Force read from master (avoid replication lag)
const user = await User.find(
{ where: { email: "john@example.com" } },
{ replicationMode: "master" }
);

Force Slave for Reads

Explicitly use slaves even when you have control flow that might default to master:

const users = await User.find(
{},
{ replicationMode: "slave" }
);

With Query Builder

The replication mode also works with the query builder using setReplicationMode():

// Force master for this read
const users = await User.query()
.setReplicationMode("master")
.where("active", true)
.many();

// Force slave for this read
const count = await User.query()
.setReplicationMode("slave")
.getCount();

Important Considerations

Replication Lag

Slave databases may have a slight delay in receiving updates from the master. This is called replication lag.

// This pattern can cause issues due to replication lag:
const user = await User.insert({ name: "John" }); // Writes to master

// This might not find the user if slave hasn't replicated yet!
const found = await User.findOne({
where: { id: user.id }
}); // Reads from slave

// Solution: Force master read after write
const found = await User.findOne(
{ where: { id: user.id } },
{ replicationMode: "master" } // Read from master
);

Transactions

All operations within a transaction use the master database, regardless of replication settings:

await sql.startTransaction(async (trx) => {
// Both operations use master
const user = await User.insert({ name: "John" }, { trx });
const found = await User.findOne({ where: { id: user.id } }, { trx });
});

No Slaves Configured

If no slaves are configured, all operations automatically fall back to the master:

const sql = new SqlDataSource({
// ... connection config
replication: {
slaves: [],
},
});

// All operations use master
const users = await User.find(); // Uses master

Streaming Operations

Streaming operations also respect replication settings:

// Streams from slave
const stream = await User.query().stream();

// Streams from master
const stream = await User.query()
.setReplicationMode("master")
.stream();

Raw Queries

When using raw queries, write operations always use master, and read operations use slaves:

// Uses master (INSERT)
await sql.query("users").insert({ name: "John", email: "john@example.com" });

// Uses slave (SELECT)
const users = await sql.query("users").many();

// Force master
const users = await sql.query("users")
.setReplicationMode("master")
.many();

Connection Management

All slave connections are automatically managed:

// Connects to master and all slaves
await sql.connect();

// Disconnects from master and all slaves
await sql.disconnect();

If a slave fails to connect during initialization, an error will be thrown. Ensure all slave databases are accessible before connecting.