Skip to main content

Split Read & Write Requests

Overview

Read/write splitting routes read operations to read-capable infrastructure and write operations to the primary write path.

TeaQL is a good fit for read/write splitting because all query and persistence operations execute through UserContext. The context can decide which data source, repository, or connection policy should be used for the current operation.

Common reasons to split reads and writes:

  • Reduce load on primary database
  • Use read replicas for reporting
  • Route analytical reads to a separate database
  • Keep writes strongly consistent
  • Support multi-region deployments
  • Isolate expensive queries

Read vs Write Operations

OperationTypical Route
Q.orders().comment("Query orders").purpose("Load data").executeForList(ctx)Read replica
Aggregation queryRead replica or analytical database
entity.save(ctx)Primary database
entity.markToRemove(); entity.save(ctx)Primary database
Ensure schema/indexPrimary database or deployment admin connection
Audit writePrimary database or audit store

Keep routing decisions behind CustomUserContext:

TeaQL request
-> CustomUserContext determines operation type
-> routing policy chooses data source
-> repository executes

Example routing service:

public interface DataSourceRoutingPolicy {

DataSourceKey routeRead(CustomUserContext userContext, String entityType);

DataSourceKey routeWrite(CustomUserContext userContext, String entityType);
}

Expose it:

public class CustomUserContext extends UserContext {

public DataSourceRoutingPolicy routingPolicy() {
return getBean(DataSourceRoutingPolicy.class);
}

public DataSourceKey readDataSource(String entityType) {
return routingPolicy().routeRead(this, entityType);
}

public DataSourceKey writeDataSource(String entityType) {
return routingPolicy().routeWrite(this, entityType);
}
}

Adapt DataSourceKey to your project.


Consistency Policy

Read/write splitting must handle replication lag.

Common consistency rules:

ScenarioRecommended Route
Read after write in same requestPrimary
User opens just-created objectPrimary or sticky primary
Reporting pageReplica
Export jobReplica or analytical database
Permission checkPrimary or strongly consistent cache
Audit verificationPrimary/audit store

CustomUserContext can keep a request-scoped flag:

public void markWriteHappened() {
putLocalStorage("writeHappened", Boolean.TRUE);
}

public boolean shouldReadFromPrimary() {
return Boolean.TRUE.equals(getLocalStorage("writeHappened"));
}

Then routing can use it:

public DataSourceKey readDataSource(String entityType) {
if (shouldReadFromPrimary()) {
return DataSourceKey.PRIMARY;
}
return routingPolicy().routeRead(this, entityType);
}

Transaction Boundaries

Inside a write transaction, reads should usually go to the primary database.

Recommended rule:

if current transaction is write transaction:
route reads to primary
else:
route reads according to read policy

This prevents a service from saving an entity and then reading stale data from a replica.


Example: Query on Replica

public List<Order> loadOrderReport(CustomUserContext userContext, OrderReportRequest request) {
userContext.useReadOnlyRoute("Order");

return Q.orders().withCreateTimeBetween(request.getStart(), request.getEnd())
.orderByCreateTimeDescending()
.comment("Query orders").purpose("Load data")
.executeForList(userContext);
}

The method name useReadOnlyRoute is illustrative. In your project, implement route selection using your repository or data-source integration.


Example: Force Primary After Write

public Order createAndReload(CustomUserContext userContext, OrderCreationRequest request) {
Order order = OrderUtil.createFrom(request);
order.auditAs("Save order").save(userContext);

userContext.markWriteHappened();

return Q.orders()
.filterById(order.getId())
.comment("Query orders")
.purpose("Reload the order after write")
.executeForOne(userContext);
}

Best Practices

  • Route all writes to primary.
  • Route read-after-write to primary.
  • Keep routing policy in CustomUserContext or a service resolved by it.
  • Make replica usage explicit for reporting and export workloads.
  • Include tenant and region in routing decisions.
  • Monitor replication lag.
  • Do not route permission-critical reads to stale replicas unless the design accepts it.

Testing Checklist

  • Normal list queries can use read route.
  • Save operations always use write route.
  • Read-after-write returns fresh data.
  • Transactional service methods do not accidentally read stale replicas.
  • Tenant routing works in multi-tenant deployments.
  • Replica outage falls back according to project policy.

Summary

Read/write splitting is powerful but must be consistency-aware. TeaQL centralizes the decision through CustomUserContext, so query code remains clean while the project controls routing, replica behavior, transaction safety, and failover policy.