What Is Database Read / Write Splitting Used For?
A beginner-to-production walkthrough of one of the simplest and most powerful tricks for scaling relational databases — explained with real-life analogies, Java code, and diagrams from Netflix-scale systems down to your first side project.
Introduction & History
The oldest and most reliable way to make a busy database keep up with a crowd is not to buy a bigger machine — it is to give the read traffic its own set of machines. That single, unglamorous idea is what this article is really about.
Imagine a small bakery with one counter. One person takes orders, bakes the bread, and hands it over — all at the same counter. When there are only five customers a day, this works fine. But when five hundred customers show up every hour, that single counter becomes the bottleneck. People wait in a long line just to ask “do you have brown bread today?” — a question that does not even need the baker, just someone who knows what is on the shelf.
A database server has the exact same problem. Every application — a shopping website, a banking app, a food delivery service — needs to talk to a database to store new information (writes) and to look up existing information (reads). In most applications, the number of times people read data (viewing a product page, checking an order status, browsing a feed) is far larger than the number of times they write data (placing an order, updating a profile, posting a comment). Studies of typical web applications commonly show a read-to-write ratio anywhere from 80:20 to as high as 99:1.
Database read / write splitting is the architectural pattern of sending write operations (INSERT, UPDATE, DELETE) to one database server — called the primary (or master) — and sending read operations (SELECT) to one or more separate database servers called replicas (or read replicas, historically also called “slaves,” a term the industry has largely retired in favour of “replica”). It is one of the oldest and most battle-tested techniques for scaling a relational database beyond what a single server can handle.
The idea is not new. Database replication itself dates back to the 1970s and 1980s in research on distributed databases, and by the late 1990s and early 2000s, as web traffic exploded, MySQL’s native asynchronous replication (introduced around MySQL 3.23 in 2000) made read / write splitting practical and popular for ordinary web companies, not just banks and telecoms. PostgreSQL added streaming replication in version 9.0 (2010), and Oracle had offered similar capabilities through Oracle Data Guard for years before that. Today, every major relational database — MySQL, PostgreSQL, SQL Server, Oracle — and every major cloud provider (AWS RDS, Google Cloud SQL, Azure Database) supports read replicas as a first-class, mostly point-and-click feature.
Think of a library. There is one librarian who is allowed to add new books to the shelves and update the catalogue (the “write” librarian). But there are five other people standing near photocopied versions of the catalogue who can only help you find and read a book (the “read” helpers). If ten people want to browse the catalogue, they do not need to interrupt the one librarian who is busy shelving new arrivals — they can each look at their own copy.
1.1 Why this pattern survived decades of technology change
It is worth pausing on just how durable this idea has been. In the last twenty-five years, the database world has seen the rise of NoSQL stores, NewSQL systems, distributed SQL databases like CockroachDB and Google Spanner, and countless new storage engines. Yet read / write splitting has not been replaced — it has simply been absorbed into almost every one of these newer systems as a built-in capability. The reason is simple: the underlying shape of the problem (many more reads than writes, and a desire to protect the “source of truth” from read pressure) has not changed, even though the tools used to solve it keep getting better.
Early implementations required a database administrator to hand-configure replication using raw configuration files, manually track binary log positions, and write custom application logic to decide where each query should go. Today, a junior developer can spin up a fully working primary-replica setup with automated failover in under ten minutes using a managed cloud console. This drop in operational cost is a major reason read / write splitting moved from being an “enterprise-only” technique reserved for banks and telecom companies to something even small startups adopt as soon as their database starts showing signs of read pressure.
1.2 Where this fits in the bigger picture of scaling
Read / write splitting is just one tool in a much larger toolbox that architects reach for when a system starts to strain under load. Caching (storing frequently requested data in fast memory stores like Redis) reduces the number of queries that ever reach the database at all. Content Delivery Networks (CDNs) push static or semi-static content physically closer to users. Sharding splits data itself across multiple independent databases. Read / write splitting sits comfortably alongside all of these — it specifically targets the database tier, and specifically targets the read side of that tier, without requiring you to change your data model or your consistency guarantees for writes.
The Problem & Motivation
To understand why read / write splitting exists, we need to understand what happens to a single database server as an application grows.
2.1 The single-server bottleneck
A single database server has a fixed amount of CPU, memory, disk I/O, and network bandwidth. Every query — whether it is reading a product’s price or updating a user’s shipping address — competes for the same pool of resources. As traffic grows, three things typically happen, usually in this order:
- CPU saturation: Complex SELECT queries with joins, sorting, and aggregation start eating CPU that write transactions also need.
- Lock contention: Long-running read queries can hold locks or consume buffer pool space that write transactions need, slowing everything down.
- I/O contention: Both reads and writes fight for the same disk I/O throughput, and disk is usually the slowest part of the system.
Eventually, response times climb, timeouts increase, and the application feels “slow” even though nothing is technically broken — the database is simply out of headroom.
2.2 Why not just get a bigger server?
The first instinct is vertical scaling: buy a bigger machine with more CPU cores, more RAM, faster SSDs. This works for a while, but it has hard limits. Beyond a certain point, hardware gets disproportionately expensive, and eventually you hit a ceiling — there simply is not a bigger machine to buy. Vertical scaling also creates a single point of failure: if that one powerful machine goes down, the entire application goes down with it.
2.3 The read-heavy reality of most applications
Here is the key insight that makes read / write splitting so effective: for most applications, reads vastly outnumber writes. A news website might have a million people reading an article for every one journalist who publishes it. An e-commerce site might have thousands of people browsing a product for every one person who actually buys it. If most of the database’s work is reads, and reads do not strictly need the absolute latest write in real time for most use cases (viewing a product listing, browsing a news feed), then we can offload that read traffic to separate, cheaper machines — freeing the primary server to focus purely on the smaller, more critical stream of writes.
The blog
A blog with one author and ten thousand daily readers. The author writes maybe 2 posts a day. Ten thousand readers refresh the homepage, click into articles, and load comments constantly. Splitting reads onto replica servers means the blog can serve ten thousand readers smoothly without those readers ever competing with the author’s occasional writes.
The e-commerce platform
An e-commerce platform like Amazon has millions of people browsing product pages every minute, but relatively far fewer checkout transactions happening at the exact same second. Product catalogue reads are routed to a fleet of read replicas, while the smaller, more sensitive stream of order-placement writes goes to the primary database, which can then be tuned, monitored, and protected far more tightly.
2.4 What happens without read / write splitting
Teams that delay adopting this pattern usually discover the need for it the hard way — through an incident. A typical story looks like this: a marketing team runs a big promotional campaign, traffic to the product listing page spikes tenfold, and suddenly customers report that placing an order or logging in has become painfully slow, or fails outright with timeout errors. On investigation, the database CPU is pegged at 100 percent, and almost all of that load is coming from the exact same handful of SELECT queries being run over and over by browsing traffic. The write path — order creation — was never the actual problem, but it suffers collateral damage because it shares the same server, the same CPU cores, and the same disk I/O queue as the flood of read queries.
This is the exact failure mode read / write splitting is designed to prevent. By giving read traffic its own dedicated hardware, a traffic spike in browsing behaviour no longer has the power to degrade the checkout experience, because the two workloads are physically isolated onto different machines.
2.5 Quantifying the read-heavy skew
It helps to internalise just how skewed real-world read / write ratios usually are. Social media feeds, search engines, content platforms, and e-commerce catalogues typically see read-to-write ratios well above 90:10, and sometimes as extreme as 99:1 or higher. Even “transactional” systems like banking apps, where every rupee matters, still see far more balance-check reads than actual money-moving writes. This consistent, near-universal skew across industries is precisely why read / write splitting generalises so well — it is not a niche trick for one type of company, but a near-default expectation for any application that serves more than a modest amount of traffic.
Core Concepts
Before going further, let us define the vocabulary clearly. Every term below will reappear throughout this article.
Primary (Master)
The single database server that accepts all write operations (INSERT, UPDATE, DELETE, DDL). It is the “source of truth.”
Replica (Read replica / slave)
A copy of the primary database that continuously receives changes from the primary and serves read-only (SELECT) queries.
Replication
The process by which changes made on the primary are copied over to one or more replicas, typically via a stream of change logs.
Replication Lag
The delay between when a change is committed on the primary and when that same change becomes visible on a replica.
Read / Write Splitting
The routing logic — inside the application, a driver, or a proxy — that sends writes to the primary and reads to replicas.
Eventual Consistency
The guarantee that, given enough time with no new writes, all replicas will reflect the same data as the primary — but not necessarily instantly.
3.1 Synchronous vs. asynchronous replication
This is the single most important concept to understand, because it dictates every trade-off in this article.
| Aspect | Synchronous replication | Asynchronous replication |
|---|---|---|
| How it works | Primary waits for the replica to confirm it received and applied the change before telling the client the write succeeded. | Primary commits the write and immediately tells the client it succeeded, then sends the change to replicas in the background. |
| Consistency | Strong — replica is always caught up when write completes. | Eventual — replica might lag by milliseconds to seconds (or more under load). |
| Write latency | Higher, since primary waits on network round-trip to replica. | Lower — primary does not wait. |
| Typical usage | Financial systems, or “semi-sync” setups for at least one replica. | The vast majority of read replicas used for scaling reads (MySQL, PostgreSQL, cloud RDS replicas by default). |
Most read / write splitting setups you will encounter in the wild — including AWS RDS Read Replicas, Google Cloud SQL Read Replicas, and typical MySQL / PostgreSQL replication — use asynchronous replication. This is precisely why “read your own write” problems exist, which we will cover in detail in Section 6 and Section 9.
There is also a middle-ground option called semi-synchronous replication, supported by MySQL, where the primary waits for at least one replica to acknowledge receiving (not necessarily fully applying) the change before confirming the write to the client. This reduces, but does not eliminate, the chance of losing recently committed data if the primary crashes immediately after a write, while keeping write latency much lower than fully synchronous setups involving every replica.
3.2 CAP theorem context
Read replicas are a practical, real-world expression of the trade-offs described by the CAP theorem (Consistency, Availability, Partition tolerance — you cannot have all three perfectly at once). By choosing asynchronous replication, teams are explicitly favouring availability and low write latency over strict, immediate consistency across all copies of the data. That is a conscious, well-understood trade-off, not a flaw.
3.3 Consistency models you will encounter
| Model | Guarantee | Where it shows up |
|---|---|---|
| Strong consistency | Every read reflects the most recent write, immediately. | Reading from the primary itself. |
| Read-your-own-writes consistency | A user always sees the effects of their own writes, even if other users might briefly see stale data. | Sticky routing right after a write, session tokens. |
| Monotonic read consistency | Once a user has seen a certain version of the data, they never see an older version afterward. | Pinning a user session to the same replica for a request window. |
| Eventual consistency | Given enough time with no further writes, all replicas converge to the same state. | General browsing traffic hitting any available replica. |
Understanding which consistency model a given feature actually needs — rather than assuming every screen in your application needs the strongest possible guarantee — is what separates a naive read / write splitting implementation from a well-engineered one. Most screens in most applications are perfectly happy with eventual consistency; only a small subset of user-facing flows genuinely require read-your-own-writes or monotonic guarantees.
3.4 Algorithms and data structures behind replication
Under the hood, replication relies on a few recurring computer science ideas worth naming explicitly:
- Append-only logs. Both MySQL’s binlog and PostgreSQL’s WAL are, at their core, append-only sequences of records — the same fundamental structure that powers technologies like Apache Kafka. Append-only writes are fast (sequential disk I/O) and make it trivial to resume replication from any known position after a disconnect.
- Log sequence numbers (LSNs). Every change is tagged with a monotonically increasing identifier. Replicas track “how far” they have applied, and this same number is what powers lag calculation (primary’s latest LSN minus replica’s applied LSN).
- Idempotent replay. Replication logic is designed so that re-applying the same log entry twice (which can happen after a crash and reconnect) does not corrupt data — an important property borrowed from distributed systems theory around at-least-once delivery.
- Consistent hashing / round-robin selection. The load balancing layer choosing which replica serves a given read often uses the same consistent-hashing or round-robin algorithms found in general distributed load balancing, discussed further in Section 8.3.
Architecture & Components
A production read / write splitting setup typically has four moving parts working together.
Primary database
Handles all writes, and often serves some reads too (especially reads that must be perfectly fresh).
Replica pool
One or more read-only copies, usually placed behind a load balancer for even distribution.
Routing layer
Either application code, an ORM feature, a driver-level router, or a dedicated proxy (like ProxySQL or PgBouncer with routing rules) that decides where each query goes.
Replication channel
The mechanism (binlog streaming in MySQL, WAL streaming in PostgreSQL) that carries changes from primary to replicas.
Notice the dotted lines from primary to each replica — that is the continuous, ongoing replication stream that keeps replicas up to date. Notice also that this happens completely independently of, and slightly after, the application’s own write.
4.1 Where routing decisions are made
| Layer | Description | Example |
|---|---|---|
| Application code | Developer manually picks a DataSource / connection based on query type. | Custom Spring @Transactional(readOnly = true) based routing. |
| ORM / Framework | Framework automatically detects read-only transactions and routes accordingly. | Spring’s AbstractRoutingDataSource, Hibernate read-only sessions. |
| Driver-level | Database driver itself is replica-aware and load-balances reads. | MySQL Connector/J with replication-aware URLs; PgBouncer. |
| Proxy / Middleware | A dedicated network proxy inspects SQL and routes transparently, with zero app changes. | ProxySQL, MaxScale, Vitess, Amazon RDS Proxy. |
How It Works Internally
Let us go one level deeper into the mechanics of replication itself, since this is the engine that makes read / write splitting possible.
5.1 MySQL: binary log (binlog) replication
- A client sends a write (say,
UPDATE orders SET status='SHIPPED' WHERE id=42) to the primary. - The primary applies the change to its own storage engine (InnoDB) and simultaneously writes an entry describing that change into its binary log — an append-only, ordered record of every change.
- The primary immediately returns success to the client (in the common asynchronous mode). The client’s write is now “durable” on the primary.
- Each replica runs an I/O thread that continuously connects to the primary and copies new binlog entries into its own local relay log.
- Each replica runs a separate SQL thread (or, in modern MySQL, multiple parallel apply threads) that reads the relay log and re-applies those same changes to its own copy of the data.
This means there are two sources of delay: network transfer time to ship the binlog, and apply time on the replica to actually execute the change. Combined, this is what we call replication lag.
5.2 PostgreSQL: write-ahead log (WAL) streaming
PostgreSQL works on a similar principle but uses its Write-Ahead Log (WAL). Every change is first written to the WAL before being applied to the actual data files — this is what guarantees durability and crash recovery. In streaming replication, PostgreSQL simply ships these WAL records over the network to replicas, which replay them to stay in sync. PostgreSQL also supports “logical replication,” which streams change events at the row level rather than the physical byte level, offering more flexibility (e.g. replicating only certain tables).
5.3 What “read-only” actually means on a replica
A replica typically runs in a special read-only mode enforced by the database engine itself: any attempt to run an INSERT, UPDATE, or DELETE directly against a replica is rejected with an error. This is an important safety guarantee — it prevents a misconfigured application from accidentally corrupting a replica’s state by writing to it directly instead of going through the primary.
In Spring Boot, this read-only enforcement is often mirrored at the application layer using @Transactional(readOnly = true). This does not just document intent — for JDBC drivers that support it, it can trigger driver-level optimisations (skipping certain transaction bookkeeping) and, combined with a routing DataSource, tells the routing layer “this call is safe to send to a replica.”
5.4 Concurrency considerations on the replica
A subtle but important detail: applying replicated changes on a replica is itself a concurrency problem. Early versions of MySQL replication applied changes using a single SQL thread, meaning the replica could only apply one change at a time, in strict order — this made it easy for a replica to fall behind a busy primary that was processing many writes in parallel across multiple connections. Modern MySQL (5.7+) and PostgreSQL both support parallel replication apply, where multiple worker threads apply non-conflicting changes concurrently (for example, changes to different, unrelated tables or rows can safely be applied out of order relative to each other), while changes that must remain strictly ordered (like multiple updates to the same row) are still serialised correctly. This parallelism is one of the main reasons replication lag has dropped dramatically on well-tuned modern database versions compared to a decade ago.
5.5 Networking considerations
The replication stream is, at the end of the day, just a TCP connection carrying a continuous feed of log data. This means ordinary networking concerns apply directly: bandwidth between primary and replica, network latency (especially significant for cross-region replicas), and connection stability. A replica in the same data centre as the primary might see replication lag measured in single-digit milliseconds, while a replica on another continent might realistically see hundreds of milliseconds of lag purely from speed-of-light network transfer time, even before accounting for apply time.
Data Flow & Lifecycle
Let us trace a single write and a single read through the whole system, end to end.
This diagram illustrates the single most famous gotcha in read / write splitting: the read-your-own-write problem. The user placed an order successfully, but if the very next request reads from a replica that has not caught up yet, the order may appear to not exist for a brief moment. We will cover mitigation strategies for this in Section 9 and Section 16.
6.1 Typical lifecycle of a read request
- Application receives a request that only needs to fetch data (e.g. “show me this product”).
- The routing layer identifies this as a read-only operation (via annotation, SQL parsing, or explicit code path).
- The routing layer picks one replica from the pool — usually using round-robin, least-connections, or weighted load balancing.
- The query executes against the replica and returns the result — completely independent of the primary’s current load.
6.2 Typical lifecycle of a write request
- Application receives a request that changes data (e.g. “place this order”).
- The routing layer sends this directly to the primary — no ambiguity here.
- The primary executes the transaction, commits it, and returns success.
- In parallel (not blocking the response), the primary streams this change out to all replicas.
Advantages, Disadvantages & Trade-offs
Every scaling decision is a set of trade-offs, and read / write splitting is no exception. The benefits are large and well proven, but they come attached to a set of operational costs and correctness concerns worth naming clearly.
7.1 Advantages
| Benefit | Why it matters |
|---|---|
| Horizontal read scalability | Add more replicas as read traffic grows, almost without limit, without touching the primary. |
| Protects the primary | Keeps the primary’s resources dedicated to the smaller, more critical stream of writes. |
| Improved read latency | Replicas can be placed geographically closer to users, reducing round-trip time. |
| Better fault isolation | A slow or expensive analytical query can be routed to a dedicated replica without ever affecting checkout or login flows. |
| Cost efficiency | Read replicas are often cheaper to add than scaling up a single giant primary machine. |
| Disaster recovery bonus | A replica can often be promoted to primary if the original primary fails. |
7.2 Disadvantages & trade-offs
| Challenge | Explanation |
|---|---|
| Replication lag | Reads may return slightly stale data — usually milliseconds, but can spike to seconds under heavy load. |
| Increased complexity | You now have routing logic, more servers to manage, and more failure modes to reason about. |
| Read-your-own-write issues | Users may not immediately see the effect of their own action if routed to a lagging replica. |
| Does not scale writes | All writes still funnel through one primary — this pattern only helps with read-heavy bottlenecks. |
| Operational overhead | More servers means more monitoring, patching, backups, and failover automation to maintain. |
| Cost | Each replica is a running server that costs money, even if it is cheaper than scaling the primary. |
Read / write splitting solves read scalability, not write scalability. If your bottleneck is actually too many writes (e.g. a high-frequency trading system or an IoT ingestion pipeline), you need a different pattern entirely — such as sharding (partitioning data across multiple primaries) — which is a separate topic from this article.
Performance & Scalability
The scaling story of read / write splitting is straightforward but worth quantifying. Suppose an application receives 10,000 queries per second, with a 90:10 read-to-write ratio. That is 9,000 reads / sec and 1,000 writes / sec. Without splitting, a single server must absorb all 10,000 queries / sec. With splitting across, say, 4 replicas, each replica only needs to handle roughly 2,250 reads / sec, while the primary only handles the 1,000 writes / sec (plus replication overhead). This dramatically lowers the load per machine and creates headroom for growth.
8.1 Horizontal scaling of reads
Because replicas are largely independent read-only copies, you can typically add more of them almost linearly to absorb more read traffic — this is a textbook case of horizontal scaling. Most cloud providers let you add a new read replica with a single API call or console click, and it will be caught up and serving traffic within minutes for moderately sized databases.
8.2 Connection pooling matters even more here
With multiple database endpoints (one primary, N replicas), connection pool sizing becomes more important. A common mistake is to open one giant connection pool per replica without capping total connections, which can overwhelm smaller replica instances. Tools like HikariCP (the default in Spring Boot) let you configure a separate, appropriately-sized pool for each DataSource.
A useful rule of thumb, borrowed from HikariCP’s own sizing guidance, is that connection pool size should be driven by the number of CPU cores available to the database server, not by the number of concurrent application threads — a surprisingly common misconception. Oversized pools do not make a database faster; past a certain point they simply create more contention for the same limited CPU and I/O resources, actually reducing throughput. This principle applies independently to the primary’s pool and to each replica’s pool, since each is backed by its own physical (or virtual) machine with its own resource ceiling.
8.3 Load balancing strategies across replicas
| Strategy | How it works | Best for |
|---|---|---|
| Round robin | Requests cycle evenly through the replica list. | Simple, uniform workloads. |
| Least connections | New requests go to the replica with fewest active connections. | Uneven query costs. |
| Weighted | Bigger / faster replicas get proportionally more traffic. | Mixed-size replica fleets. |
| Lag-aware routing | Replicas reporting high lag are temporarily removed from rotation. | Consistency-sensitive reads. |
High Availability & Reliability
Read replicas earn their keep twice: once for scale, and once for resilience. The same infrastructure that protects the primary from read pressure also stands ready to become the new primary if the current one fails.
9.1 Handling replication lag gracefully
Since asynchronous replicas can lag, applications need strategies to cope:
- Read-after-write routing. Immediately after a write, route the next few related reads to the primary itself (a “sticky” session), rather than a replica, until enough time has passed.
- Monitor lag and exclude stale replicas. A health check tracks each replica’s lag (e.g.
SHOW SLAVE STATUSin MySQL, orpg_stat_replicationin PostgreSQL) and removes replicas exceeding a lag threshold from the routing pool automatically. - Session consistency tokens. Some systems pass a “last write position” token to the client, which is then used to ensure subsequent reads only hit a replica that has caught up to at least that position.
9.2 Failover: what happens if the primary dies?
This is where replicas earn their keep twice over. If the primary server crashes, one of the replicas can be promoted to become the new primary. Modern managed database services (AWS RDS Multi-AZ, Google Cloud SQL HA) automate this promotion, typically completing failover within 30–120 seconds. Self-managed setups often use tools like Orchestrator (for MySQL) or Patroni (for PostgreSQL) to automate the same process.
9.3 Reliability checklist
Automated backups
Independent of replication — always keep point-in-time recoverable backups of the primary.
Multi-AZ / multi-region replicas
Spread replicas across failure domains so one data centre outage does not take down all read capacity.
Circuit breakers
If replicas are unreachable, applications should gracefully fall back to the primary rather than failing outright (with care around load).
Regular failover drills
Practising promotion in a staging environment builds confidence it will work under real pressure.
Security
Read / write splitting introduces more network paths and more machines, each of which needs to be secured properly.
- Encrypt replication traffic. Use TLS for the replication channel between primary and replicas, especially across data centres or cloud regions.
- Separate credentials. Give the application a read-only database user for the replica connection pool, so even a routing bug cannot accidentally issue a write against a replica (the DB engine blocks it anyway, but least-privilege is good defense in depth).
- Network isolation. Place replicas in private subnets, reachable only from application servers, never directly from the public internet.
- Audit logging. Since replicas often serve reporting or analytics tools, ensure query auditing captures who accessed what, especially for sensitive data (PII, financial records).
- Patch consistently. Every additional replica is another server that needs security patches — automate this rather than doing it manually per-instance.
A replica contains a full copy of your production data — meaning a security breach on a “less important” read replica can be just as damaging as a breach on the primary. Never treat replicas as lower-security-tier systems.
Monitoring, Logging & Metrics
The single most important metric to watch in any read / write splitting setup is replication lag, typically measured in seconds or milliseconds behind the primary.
| Metric | What it tells you |
|---|---|
| Replication lag (seconds behind primary) | How stale reads on a given replica might be right now. |
| Replica CPU / memory / disk I/O | Whether a replica is overloaded and needs to be scaled or added to. |
| Read / write query ratio | Confirms your traffic assumptions and helps size the replica pool. |
| Connection pool saturation (per DataSource) | Whether the app is starving for connections to primary or replicas. |
| Failed routing / fallback events | How often reads fall back to the primary due to replica unavailability. |
| Replica promotion events | Tracks failover history for post-incident review. |
Tools like Prometheus with mysqld_exporter or postgres_exporter, combined with Grafana dashboards, are the industry-standard way to visualise these metrics. Distributed tracing (with correlation IDs threaded through requests) also helps confirm, in production, which specific database instance actually served a given query — invaluable when debugging a “why did the user see stale data” ticket.
Deployment & Cloud
Nearly every managed cloud database service supports read replicas as a built-in feature, which has made this pattern far more accessible than it was 15 years ago when teams had to hand-configure replication themselves.
AWS RDS / Aurora
Read Replicas with one-click creation; Aurora even supports up to 15 low-latency replicas sharing distributed storage.
Google Cloud SQL
Read replicas across zones and regions, with automated replica management.
Azure Database
Read replicas for MySQL, PostgreSQL, and geo-replication for cross-region reads.
Kubernetes / operators
Operators like Vitess (MySQL), Patroni (PostgreSQL), or Percona XtraDB Cluster automate replica provisioning and failover on your own infrastructure.
When deploying, a common practice is to place replicas in different availability zones (for resilience) or even different geographic regions (to serve users closer to them with lower latency), while keeping the primary in the region where most writes originate.
Java Implementation Deep Dive
Let us build a realistic Spring Boot example that automatically routes reads to a replica and writes to the primary, using Spring’s built-in AbstractRoutingDataSource.
13.1 Step 1 — define a context holder for the current route
public class DbContextHolder {
public enum DbType { PRIMARY, REPLICA }
private static final ThreadLocal<DbType> CONTEXT = new ThreadLocal<>();
public static void setDbType(DbType dbType) {
CONTEXT.set(dbType);
}
public static DbType getDbType() {
return CONTEXT.get() == null ? DbType.PRIMARY : CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}13.2 Step 2 — create the routing DataSource
public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// Returns PRIMARY or REPLICA - Spring uses this key
// to pick the right underlying DataSource from the map.
return DbContextHolder.getDbType();
}
}13.3 Step 3 — wire up the primary and replica DataSources
@Configuration
public class DataSourceConfig {
@Bean
public DataSource primaryDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://primary-db-host:3306/orders");
config.setUsername("app_write_user");
config.setPassword("secret");
config.setMaximumPoolSize(20);
return new HikariDataSource(config);
}
@Bean
public DataSource replicaDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://replica-db-host:3306/orders");
config.setUsername("app_read_only_user");
config.setPassword("secret");
config.setMaximumPoolSize(30); // often larger, since reads dominate
config.setReadOnly(true);
return new HikariDataSource(config);
}
@Bean
@Primary
public DataSource routingDataSource(
@Qualifier("primaryDataSource") DataSource primary,
@Qualifier("replicaDataSource") DataSource replica) {
ReadWriteRoutingDataSource routingDataSource = new ReadWriteRoutingDataSource();
Map<Object, Object> targets = new HashMap<>();
targets.put(DbContextHolder.DbType.PRIMARY, primary);
targets.put(DbContextHolder.DbType.REPLICA, replica);
routingDataSource.setTargetDataSources(targets);
routingDataSource.setDefaultTargetDataSource(primary); // safe default
return routingDataSource;
}
}13.4 Step 4 — automatically route based on transaction type using AOP
@Aspect
@Component
@Order(0) // must run before @Transactional's own advice
public class ReadWriteRoutingAspect {
@Around("@annotation(transactional)")
public Object route(ProceedingJoinPoint joinPoint,
Transactional transactional) throws Throwable {
try {
if (transactional.readOnly()) {
DbContextHolder.setDbType(DbContextHolder.DbType.REPLICA);
} else {
DbContextHolder.setDbType(DbContextHolder.DbType.PRIMARY);
}
return joinPoint.proceed();
} finally {
DbContextHolder.clear();
}
}
}13.5 Step 5 — use it naturally in service code
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
// Routed to the PRIMARY automatically
@Transactional
public Order placeOrder(Order order) {
return orderRepository.save(order);
}
// Routed to a REPLICA automatically
@Transactional(readOnly = true)
public Order getOrder(Long id) {
return orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
}For endpoints where freshness truly matters right after a write (like showing the newly placed order confirmation page), you can deliberately force that specific read to the primary, overriding the default routing, instead of relying on readOnly = true: simply call DbContextHolder.setDbType(DbType.PRIMARY) before the read, or mark that method as a regular (non-read-only) transaction.
APIs & Microservices Considerations
In a microservices architecture, read / write splitting typically lives inside each service that owns its own database (following the database-per-service pattern). A few extra considerations apply:
- Per-service replicas. Each microservice manages its own primary / replica pair — there is no single shared “database team” decision, which keeps services independently scalable.
- API-level read / write separation. Some teams expose this at the API gateway level, tagging endpoints as read-heavy vs. write-heavy, and routing accordingly, especially for GraphQL or REST APIs with predictable access patterns.
- Event-driven consistency. Combined with patterns like CQRS (Command Query Responsibility Segregation), read replicas can serve dedicated “query-side” models built from events, decoupling the read path even further from the transactional write path.
- Correlation IDs across DB calls. When debugging distributed traces, tag which physical database instance served each query, since a stale read on a replica might explain a confusing downstream inconsistency.
Design Patterns & Anti-Patterns
Adopting read / write splitting is really about a small family of related patterns and a matching family of failure modes to steer around. The vocabulary below is what senior engineers use when reviewing a routing design.
15.1 Good patterns
Sticky read-after-write
Route a user’s reads to the primary for a short window right after they perform a write, then fall back to replicas.
Lag-aware load balancing
Continuously monitor lag per replica and automatically deprioritise or exclude lagging ones.
CQRS with dedicated read models
Build purpose-shaped read replicas (or derived stores) optimised for specific query patterns, separate from the transactional write model.
Graceful degradation
If all replicas are unavailable, fall back to the primary (with rate limiting) rather than failing every read request.
15.2 Anti-patterns to avoid
Blind 50/50 routing
Randomly routing every query without regard to read / write type or freshness needs, causing subtle data bugs.
Ignoring replication lag
Assuming replicas are always instantly consistent, leading to confusing “ghost data” bugs that are hard to reproduce.
Writing directly to a replica
Bypassing the primary for “just this one write” — usually blocked by the DB, but attempted through misconfiguration, causing outages.
One giant shared connection pool
Not separating pools per DataSource, causing primary connections to starve when replica traffic spikes (or vice versa).
Best Practices & Common Mistakes
A short, tactical checklist of what to do — and what to avoid — when rolling read / write splitting into a real production system.
16.1 Best practices
- Default new / unclassified queries to the primary, not a replica — it is the safer failure mode.
- Explicitly mark read-only transactions in code (e.g.
@Transactional(readOnly = true)) rather than guessing based on SQL text. - Monitor replication lag continuously, and alert when it exceeds an acceptable threshold for your use case (often 1–5 seconds for most web apps).
- Design critical user flows (checkout confirmation, login state) to tolerate or explicitly avoid replica staleness.
- Load test with realistic read / write ratios before assuming a given number of replicas will be “enough.”
- Automate replica provisioning and failover — manual processes fail under pressure during real incidents.
16.2 Common mistakes
- Forgetting about read-your-own-write. A user updates their profile picture, refreshes, and sees the old one — because the read hit a lagging replica.
- Not sizing replicas for peak, not average, load. A replica sized for “average” traffic falls over during a flash sale or viral moment.
- Treating replicas as a backup strategy. Replication is not a substitute for real backups — a bad DELETE on the primary replicates to every replica too.
- No visibility into which DB served a query. Makes debugging “why is this data wrong” incidents far harder than it needs to be.
16.3 A practical checklist before going to production
Before flipping read / write splitting on for real user traffic, it is worth walking through a short readiness checklist. First, confirm that every write-triggering endpoint in your application is correctly annotated or routed to the primary — a single missed endpoint that silently sends a write to a read-only replica will fail loudly in production, so it is far better to catch it with integration tests beforehand. Second, load test the replica pool under a realistic read / write ratio, not just an even split, since real traffic almost never splits evenly. Third, set up alerting on replication lag before launch, not after the first incident — waiting for a customer complaint to discover your monitoring gap is a painful (and avoidable) way to learn this lesson. Fourth, document, for your own team, exactly which user-facing flows are allowed to tolerate slightly stale reads and which are not, so that new engineers joining the project do not have to rediscover this by trial and error.
Finally, run a game-day exercise: deliberately stop replication to one replica in a staging environment and confirm that your monitoring correctly flags rising lag, that your routing layer correctly excludes or deprioritises that replica, and that the application continues functioning correctly (perhaps with slightly reduced read capacity) rather than serving errors to users. Teams that only discover how their system behaves during a real replication failure, in production, for the first time, are taking on far more risk than necessary.
Real-World / Industry Examples
The pattern is everywhere once you start looking. Below are a handful of well-known examples, followed by a worked example that ties the whole article together.
Netflix
Uses read replicas extensively across its data tier to serve massive read volumes for browsing, recommendations metadata, and catalogue data, while keeping write paths for account and billing events isolated and tightly controlled.
Uber
Historically used MySQL with read replicas for services with heavy read patterns (like trip history and driver profile lookups), evolving toward more advanced sharded architectures as scale grew.
GitHub
Has publicly discussed using MySQL read replicas extensively to serve the read-heavy load of browsing repositories, issues, and pull requests, while keeping writes (commits, comments) funnelled to primaries per shard.
Shopify
Uses read replicas per shard in its sharded MySQL architecture to absorb storefront browsing traffic separately from checkout and inventory writes.
While the exact internal architecture of these companies evolves constantly and often combines read / write splitting with sharding, caching layers, and CQRS, the underlying principle — isolate the smaller, more sensitive write path from the much larger read path — remains a foundational building block across the industry.
17.1 A worked example: a growing food delivery startup
Consider a hypothetical food delivery startup as it scales, since walking through its journey ties everything in this article together. In its first year, it runs on a single small database server — a single primary handling everything, and life is simple. As it expands to more cities, the “restaurant browsing” screen (an intensely read-heavy feature, since every customer scrolls through dozens of restaurants before placing one order) begins consuming most of the database’s CPU during lunch and dinner rush hours, and order placement — the actual revenue-generating write — starts timing out during exactly those peak hours.
The engineering team’s first fix is to add a single read replica and route the restaurant-browsing and menu-viewing queries to it, using exactly the kind of Spring Boot routing setup shown in Section 13. Almost overnight, checkout latency during rush hour drops back to normal, because order-placement writes are no longer fighting the flood of browsing reads for CPU and I/O. As the company expands to more cities and traffic keeps growing, they add two more replicas and put lag-aware load balancing in front of them. Eventually, when order volume itself becomes large enough that even the primary struggles under write load, they begin sharding orders by city — at which point read / write splitting is applied independently within each city’s shard. This progression — single server, then read replicas, then sharding — mirrors how most real companies actually evolve their database architecture over time, rather than over-engineering for scale they do not yet have.
How This Compares to Other Scaling Techniques
Read / write splitting is often used alongside — not instead of — several other well-known scaling techniques. Understanding how they differ helps you pick the right combination for your system.
| Technique | What it actually does | Relationship to read / write splitting |
|---|---|---|
| Caching (e.g. Redis, Memcached) | Stores frequently accessed data in memory, avoiding the database entirely for repeat reads. | Complementary — caching reduces load on replicas even further, often used together as the first line of defense before a query ever reaches a replica. |
| CDN | Caches and serves static or semi-static content from servers geographically near the user. | Operates at a different layer entirely (network edge, not database), but shares the same underlying philosophy of separating hot, frequently-read content from the system of record. |
| Sharding | Splits data itself across multiple independent primary databases, each responsible for a subset of records. | Solves write scalability, which read / write splitting does not. Large systems often shard first, then apply read / write splitting to each shard’s primary independently. |
| Connection pooling | Reuses a limited set of database connections across many application requests instead of opening a new one each time. | A prerequisite, not an alternative — every DataSource in a read / write split setup (primary and each replica) needs its own well-tuned connection pool. |
| CQRS | Architecturally separates the write model (commands) from the read model (queries), sometimes using entirely different storage technologies for each. | A more extreme, more flexible evolution of the same core idea — read / write splitting is essentially “CQRS at the database replication level” rather than the full application-architecture level. |
In a mature, high-scale system, it is common to see all five of these techniques working together: a CDN in front of static assets, a cache in front of hot database rows, read replicas absorbing the remaining database read traffic, sharding distributing write load across multiple primaries, and well-tuned connection pools gluing the application to every one of these data sources efficiently.
Frequently Asked Questions
A collection of the questions that come up most often once teams start seriously considering read / write splitting for their own systems.
Does read / write splitting help scale writes too?
No. All writes still go to a single primary. If write volume itself is the bottleneck, you need sharding (splitting data across multiple independent primaries) or a different write-scaling strategy, not read / write splitting.
How much lag is “normal”?
Under healthy conditions, replication lag is typically in the tens to low hundreds of milliseconds. Under heavy write bursts or large schema changes, it can spike to several seconds — which is why continuous lag monitoring matters.
Can I write to a replica directly?
Database engines generally block direct writes to a replica configured in read-only mode, returning an error. This is intentional and protects data integrity.
Is read / write splitting the same as sharding?
No. Read / write splitting keeps all data on one logical primary and duplicates it to read replicas. Sharding partitions the data itself across multiple independent primary databases, each owning a subset of the data. The two techniques are often combined in very large systems.
Do I need this for a small application?
Usually not right away. Most small to mid-sized applications run comfortably on a single well-tuned database server for a long time. Read / write splitting is worth adopting once you have clear evidence of read-driven CPU or I/O pressure on your primary — introducing it too early adds operational complexity without real benefit.
What is the difference between “replica” and “slave”?
They refer to the same concept — a read-only copy of a primary database. “Replica” is the modern, widely preferred term across documentation and tooling (MySQL, PostgreSQL, and all major clouds have moved to this terminology).
How many read replicas should I add?
There is no fixed number — it depends on your read query volume and how much headroom each replica gives you. A practical approach is to add one replica, measure how much read traffic it absorbs comfortably under realistic load testing, and then scale the count up proportionally to your actual read traffic, leaving some buffer capacity for growth and for the loss of one replica during maintenance or failure.
Can replicas be used for anything other than serving application reads?
Yes — a very common and highly recommended pattern is to dedicate one replica specifically to reporting, analytics, or data-export jobs. These queries are often long-running and resource-intensive, and isolating them onto their own replica prevents them from ever competing with, or slowing down, either the primary or the replicas serving live user traffic.
Does read / write splitting work with ORMs like Hibernate or JPA?
Yes. Most ORMs sit on top of a standard JDBC DataSource, so a routing DataSource (as shown in Section 13) works transparently underneath them — the ORM does not need to know routing is happening at all, as long as the routing decision is made before the query reaches the database layer.
Summary & Key Takeaways
If you remember nothing else from this article, remember the single-sentence summary: reads to replicas, writes to the primary, and monitor the lag between them like your job depends on it — because sometimes it will.
Key takeaways
- Database read / write splitting routes writes to a primary database and reads to one or more read replicas, exploiting the fact that most applications are read-heavy.
- It is enabled by database replication — typically asynchronous — which streams committed changes from the primary to replicas continuously.
- The core trade-off is eventual consistency: replicas may briefly lag behind the primary, which can cause stale reads, most notably the “read your own write” problem.
- Architecturally, routing can happen in application code, an ORM, a driver, or a dedicated proxy — each with different levels of transparency and control.
- It scales reads horizontally, not writes — for write-heavy bottlenecks, other patterns like sharding are needed.
- Production-grade implementations require careful monitoring (especially replication lag), automated failover, secure replica configuration, and thoughtful handling of consistency-sensitive read paths.
- Nearly every major cloud database service supports read replicas natively, making this one of the most accessible and cost-effective scaling techniques available today.
Read / write splitting is deceptively simple to describe — “send reads here, writes there” — but building it well requires genuine understanding of replication mechanics, consistency trade-offs, and failure handling. Master this pattern, and you have one of the most reliable, widely applicable tools in a software architect’s toolbox for scaling relational databases in the real world.