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
| Operation | Typical Route |
|---|---|
Q.orders().comment("Query orders").purpose("Load data").executeForList(ctx) | Read replica |
| Aggregation query | Read replica or analytical database |
entity.save(ctx) | Primary database |
entity.markToRemove(); entity.save(ctx) | Primary database |
| Ensure schema/index | Primary database or deployment admin connection |
| Audit write | Primary database or audit store |
Recommended Design
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:
| Scenario | Recommended Route |
|---|---|
| Read after write in same request | Primary |
| User opens just-created object | Primary or sticky primary |
| Reporting page | Replica |
| Export job | Replica or analytical database |
| Permission check | Primary or strongly consistent cache |
| Audit verification | Primary/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
CustomUserContextor 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.