Skip to main content

Command Palette

Search for a command to run...

Database Replication Master-Slave

Published
8 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Database Replication: Master-Slave Architecture for Modern Applications

Metadata

{
  "seo_title": "Master-Slave Database Replication: Complete Developer Guide 2026",
  "meta_description": "Learn master-slave database replication with TypeScript examples. Explore architecture patterns, implementation strategies, common pitfalls, and best practices for scalable systems.",
  "keywords": [
    "database replication",
    "master-slave architecture",
    "database scalability",
    "read replicas",
    "TypeScript database",
    "replication lag",
    "database high availability",
    "PostgreSQL replication",
    "MySQL replication"
  ],
  "tags": [
    "Database Architecture",
    "Scalability",
    "TypeScript",
    "Distributed Systems",
    "DevOps",
    "Backend Development",
    "High Availability"
  ]
}

The Problem: Scaling Database Reads in 2026

Modern applications face unprecedented data demands. Your startup's API that served 100 requests per second last year now handles 10,000. Your e-commerce platform experiences traffic spikes during flash sales. Your SaaS product expanded globally, and users expect sub-100ms response times regardless of location.

The bottleneck? Your database.

While vertical scaling (bigger servers) offers temporary relief, it's expensive and hits physical limits. A single database server handling both reads and writes becomes a critical failure point. When 80-90% of your database operations are reads—typical for most applications—you're wasting resources and creating unnecessary contention.

Master-slave replication addresses these challenges by distributing read operations across multiple database replicas while maintaining a single source of truth for writes. This architecture pattern has evolved significantly, and in 2026, developers need to understand not just the basics, but the nuances of implementing it in cloud-native, distributed environments.

The stakes are high: improper replication strategies lead to data inconsistencies, race conditions, and user-facing bugs that erode trust. Yet when implemented correctly, master-slave replication provides horizontal scalability, improved fault tolerance, and geographic distribution—all critical for modern applications.

Understanding Master-Slave Architecture

In master-slave replication, one database server (the master or primary) accepts write operations and propagates changes to one or more slave (or replica) servers. Slaves handle read operations, distributing the query load and reducing master server burden.

Key characteristics:

  • Unidirectional data flow: Changes flow from master to slaves
  • Asynchronous or synchronous replication: Trade-offs between consistency and performance
  • Read scalability: Add slaves to handle increased read traffic
  • Write bottleneck: All writes still go through the master

Modern TypeScript Implementation

Let's build a production-ready database connection manager that intelligently routes queries to master or slave instances.

Setting Up the Infrastructure

import { Pool, PoolConfig } from 'pg';

interface ReplicationConfig {
  master: PoolConfig;
  slaves: PoolConfig[];
  replicationLag?: number; // milliseconds
  healthCheckInterval?: number;
}

enum QueryType {
  READ = 'READ',
  WRITE = 'WRITE'
}

class DatabaseReplicationManager {
  private masterPool: Pool;
  private slavePools: Pool[];
  private currentSlaveIndex: number = 0;
  private slaveHealth: Map<number, boolean> = new Map();

  constructor(private config: ReplicationConfig) {
    this.masterPool = new Pool(config.master);
    this.slavePools = config.slaves.map(slaveConfig => new Pool(slaveConfig));

    // Initialize health tracking
    this.slavePools.forEach((_, index) => {
      this.slaveHealth.set(index, true);
    });

    // Start health checks
    this.startHealthChecks();
  }

  private startHealthChecks(): void {
    const interval = this.config.healthCheckInterval || 30000;

    setInterval(async () => {
      for (let i = 0; i < this.slavePools.length; i++) {
        try {
          await this.slavePools[i].query('SELECT 1');
          this.slaveHealth.set(i, true);
        } catch (error) {
          console.error(`Slave ${i} health check failed:`, error);
          this.slaveHealth.set(i, false);
        }
      }
    }, interval);
  }

  private getHealthySlave(): Pool {
    const healthySlaves = Array.from(this.slaveHealth.entries())
      .filter(([_, healthy]) => healthy)
      .map(([index]) => index);

    if (healthySlaves.length === 0) {
      console.warn('No healthy slaves available, falling back to master');
      return this.masterPool;
    }

    // Round-robin load balancing
    this.currentSlaveIndex = (this.currentSlaveIndex + 1) % healthySlaves.length;
    return this.slavePools[healthySlaves[this.currentSlaveIndex]];
  }

  async query<T = any>(
    sql: string,
    params?: any[],
    queryType: QueryType = QueryType.READ
  ): Promise<T> {
    const pool = queryType === QueryType.WRITE 
      ? this.masterPool 
      : this.getHealthySlave();

    try {
      const result = await pool.query(sql, params);
      return result.rows as T;
    } catch (error) {
      console.error('Query execution failed:', error);
      throw error;
    }
  }

  async transaction<T>(
    callback: (client: any) => Promise<T>
  ): Promise<T> {
    const client = await this.masterPool.connect();

    try {
      await client.query('BEGIN');
      const result = await callback(client);
      await client.query('COMMIT');
      return result;
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  async close(): Promise<void> {
    await this.masterPool.end();
    await Promise.all(this.slavePools.map(pool => pool.end()));
  }
}

Handling Replication Lag

Replication lag—the delay between master writes and slave updates—is the Achilles' heel of master-slave architectures.

class ReplicationLagHandler {
  private recentWrites: Map<string, number> = new Map();

  recordWrite(entityId: string): void {
    this.recentWrites.set(entityId, Date.now());
  }

  shouldReadFromMaster(entityId: string, maxLag: number = 1000): boolean {
    const writeTime = this.recentWrites.get(entityId);

    if (!writeTime) return false;

    const elapsed = Date.now() - writeTime;

    if (elapsed > maxLag) {
      this.recentWrites.delete(entityId);
      return false;
    }

    return true;
  }
}

// Usage in a service layer
class UserService {
  constructor(
    private db: DatabaseReplicationManager,
    private lagHandler: ReplicationLagHandler
  ) {}

  async updateUser(userId: string, data: Partial<User>): Promise<User> {
    const result = await this.db.query<User>(
      'UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *',
      [data.name, data.email, userId],
      QueryType.WRITE
    );

    this.lagHandler.recordWrite(userId);
    return result[0];
  }

  async getUser(userId: string): Promise<User | null> {
    const queryType = this.lagHandler.shouldReadFromMaster(userId)
      ? QueryType.WRITE  // Read from master
      : QueryType.READ;  // Read from slave

    const result = await this.db.query<User>(
      'SELECT * FROM users WHERE id = $1',
      [userId],
      queryType
    );

    return result[0] || null;
  }
}

Session Consistency Pattern

For user sessions, implement sticky reads to ensure consistency:

class SessionAwareDatabase extends DatabaseReplicationManager {
  private sessionMasterReads: Map<string, number> = new Map();
  private readonly SESSION_MASTER_DURATION = 5000; // 5 seconds

  async queryWithSession<T = any>(
    sql: string,
    params: any[],
    queryType: QueryType,
    sessionId?: string
  ): Promise<T> {
    let effectiveQueryType = queryType;

    if (sessionId && queryType === QueryType.READ) {
      const lastMasterRead = this.sessionMasterReads.get(sessionId);

      if (lastMasterRead && Date.now() - lastMasterRead < this.SESSION_MASTER_DURATION) {
        effectiveQueryType = QueryType.WRITE; // Force master read
      }
    }

    const result = await this.query<T>(sql, params, effectiveQueryType);

    if (sessionId && effectiveQueryType === QueryType.WRITE) {
      this.sessionMasterReads.set(sessionId, Date.now());
    }

    return result;
  }
}

Common Pitfalls and How to Avoid Them

1. Ignoring Replication Lag

Problem: Reading immediately after writing returns stale data.

Solution: Implement read-your-writes consistency by routing post-write reads to the master for a configurable duration.

2. Unbalanced Load Distribution

Problem: Simple round-robin doesn't account for slave capacity or geographic proximity.

Solution: Implement weighted load balancing based on slave metrics:

private getWeightedSlave(): Pool {
  // Consider factors: CPU usage, query latency, geographic proximity
  const weights = this.slavePools.map((_, index) => 
    this.calculateSlaveWeight(index)
  );

  // Weighted random selection
  const totalWeight = weights.reduce((sum, w) => sum + w, 0);
  let random = Math.random() * totalWeight;

  for (let i = 0; i < weights.length; i++) {
    random -= weights[i];
    if (random <= 0) return this.slavePools[i];
  }

  return this.slavePools[0];
}

3. Cascading Failures

Problem: When slaves fail, all traffic redirects to master, overwhelming it.

Solution: Implement circuit breakers and graceful degradation.

4. Transaction Splitting

Problem: Accidentally splitting transactions across master and slaves.

Solution: Always route entire transactions to master, never mix connection sources within a transaction.

5. Monitoring Blindness

Problem: Not tracking replication lag or slave health.

Solution: Implement comprehensive monitoring:

interface ReplicationMetrics {
  replicationLag: number;
  slaveHealth: boolean;
  queryDistribution: { master: number; slaves: number };
  failoverCount: number;
}

async getMetrics(): Promise<ReplicationMetrics> {
  const lagQuery = `
    SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) 
    AS lag_seconds
  `;

  // Query each slave for lag
  const lagResults = await Promise.all(
    this.slavePools.map(pool => 
      pool.query(lagQuery).catch(() => ({ rows: [{ lag_seconds: null }] }))
    )
  );

  return {
    replicationLag: Math.max(...lagResults.map(r => r.rows[0]?.lag_seconds || 0)),
    slaveHealth: Array.from(this.slaveHealth.values()).every(h => h),
    queryDistribution: this.getQueryDistribution(),
    failoverCount: this.failoverCount
  };
}

Best Practices

  1. Use connection pooling: Maintain persistent connections to reduce overhead
  2. Implement retry logic: Handle transient network failures gracefully
  3. Monitor replication lag: Alert when lag exceeds acceptable thresholds (typically 1-5 seconds)
  4. Plan for failover: Automate master promotion when primary fails
  5. Test failure scenarios: Regularly simulate slave failures and network partitions
  6. Document query routing: Make it clear which queries hit which servers
  7. Use prepared statements: Improve performance and prevent SQL injection

Frequently Asked Questions

Q: When should I use master-slave replication vs. other patterns?

A: Master-slave works best for read-heavy workloads (80%+ reads). For write-heavy applications, consider multi-master replication or sharding. For strong consistency requirements, evaluate synchronous replication or distributed databases like CockroachDB.

Q: How many slaves should I deploy?

A: Start with 2-3 slaves for redundancy and load distribution. Scale based on read traffic, geographic distribution needs, and budget. Monitor CPU and I/O utilization—add slaves when existing ones consistently exceed 70% utilization.

Q: What's acceptable replication lag?

A: Depends on your application. E-commerce might tolerate 1-2 seconds for product listings but needs immediate consistency for inventory. Social media can handle 5-10 seconds for feeds. Financial applications often require <100ms or synchronous replication.

Q: How do I handle slave promotion during master failure?

A: Implement automated failover with tools like Patroni (PostgreSQL) or Orchestrator (MySQL). Ensure slaves are configured for promotion, update application connection strings via service discovery, and have runbooks for manual intervention.

Q: Should I use asynchronous or synchronous replication?

A: Asynchronous provides better performance but risks data loss during master failure. Synchronous guarantees consistency but adds latency. Consider semi-synchronous as a middle ground—wait for at least one slave acknowledgment.

Q: How do I test replication lag handling?

A: Introduce artificial delays using network simulation tools (tc, toxiproxy), write integration tests that verify read-your-writes consistency, and use chaos engineering to randomly delay replication.

Q: Can I use slaves for analytics queries?

A: Yes, but be cautious. Heavy analytical queries can impact slave performance for application reads. Consider dedicated analytics replicas with different resource allocation or use read replicas specifically for reporting.


Master-slave replication remains a foundational pattern for scaling databases in 2026. By understanding its nuances, implementing intelligent routing logic, and following best practices, you'll build systems that scale gracefully while maintaining data consistency. The TypeScript patterns shown here provide a solid foundation—adapt them to your specific database technology and requirements.