Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Route selected Spring Data repositories to a read replica by giving them a marker annotation and scanning them with a second @EnableJpaRepositories configuration. Keep the unmarked repositories on the primary EntityManagerFactory; bind the marked repositories to a read-only data source and its own EntityManagerFactory. This is explicit repository selection—it is not automatic routing from @Transactional(readOnly = true).

The architecture: two repository groups, two EntityManagers

The pattern in Emmanouil Gkatziouras’s October 10, 2019 tutorial creates two persistence paths:

Repository group Selection rule EntityManagerFactory Data source Intended use Freshness
Ordinary repositories Included by the primary scan and excluded when marked @ReadOnlyRepository Primary entityManagerFactory Primary/write database Queries, inserts, updates and deletes Authoritative primary state
Read repositories Included only when annotated @ReadOnlyRepository Secondary readEntityManagerFactory Configured by spring.datasource.readUrl Read operations that can tolerate replica lag May be behind the primary after a write

The annotation is a component-scanning selector. It does not grant database permissions, prove that a replica is healthy, or prevent a driver from accepting a write.

1. Define a repository interface with no mutation API

Use Spring Data’s base Repository interface when you want to expose only explicitly declared methods. The sample read repository exposes findAll() and deliberately does not inherit save, delete or other CRUD mutation methods.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
package com.example.employee;

import org.springframework.data.repository.Repository;
import java.util.List;

public interface ReadEmployeeRepository extends Repository<Employee, Long> {
    List<Employee> findAll();
}

This narrows what application code can call through that interface. It is still not a substitute for database-level privileges: the read connection should be granted only the permissions your deployment requires, and the application should test that policy separately.

2. Add a runtime marker annotation

Make the marker visible to Spring’s repository scanner at runtime and target it at repository interfaces.

package com.example.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ReadOnlyRepository {
}

Annotate only repositories intended for the replica:

@ReadOnlyRepository
public interface ReadEmployeeRepository extends Repository<Employee, Long> {
    List<Employee> findAll();
}

3. Configure the primary repository scan

The primary configuration scans the application’s repository package, excludes interfaces carrying @ReadOnlyRepository, and points the resulting repositories at the primary factory. Mark the primary data source and factory as @Primary so unqualified injections resolve to the write path.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
@Configuration
@EnableJpaRepositories(
    basePackages = "com.example.employee",
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.ANNOTATION,
        classes = ReadOnlyRepository.class
    ),
    entityManagerFactoryRef = "entityManagerFactory",
    transactionManagerRef = "transactionManager"
)
public class PrimaryRepositoryConfiguration {
    // Define the primary DataSource, entityManagerFactory and transactionManager.
    // Mark the DataSource and EntityManagerFactory @Primary.
}

Use the actual package containing your ordinary repositories. If the package is too broad, the two scans can overlap or pick up repositories that belong on the replica.

4. Configure the read repository scan

A second scan includes only repositories carrying the marker and binds them to the read factory and its transaction manager.

@Configuration
@EnableJpaRepositories(
    basePackages = "com.example.employee",
    includeFilters = @ComponentScan.Filter(
        type = FilterType.ANNOTATION,
        classes = ReadOnlyRepository.class
    ),
    entityManagerFactoryRef = "readEntityManagerFactory",
    transactionManagerRef = "readTransactionManager"
)
public class ReadRepositoryConfiguration {
    // Define the read DataSource, readEntityManagerFactory and readTransactionManager.
}

Both scans may use the same base package because their include and exclude filters partition the interfaces. Separate packages are also possible, but the selection rule must remain unambiguous.

5. Supply a second data source and EntityManagerFactory

The read factory must be built from a data source that points at the replica. The tutorial uses a separate spring.datasource.readUrl setting for that connection. Bind the same JPA entity packages and vendor properties needed by the primary factory, then create a distinct transaction manager for the read factory.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
spring.datasource.url=jdbc:postgresql://primary.example/people
spring.datasource.readUrl=jdbc:postgresql://replica.example/people

The exact bean definitions depend on your Spring Boot and Spring Data versions. Verify current APIs, bean names, driver settings, dialect configuration, and transaction-manager wiring against the versions running in your application; the 2019 tutorial does not pin a complete Boot, Java, JDBC-driver or PostgreSQL version set.

6. Inject the repository that matches the operation

Keep write workflows on the ordinary repository and use the marked repository for replica-tolerant reads.

@RestController
class EmployeeController {
    private final EmployeeRepository employeeRepository;
    private final ReadEmployeeRepository readEmployeeRepository;

    EmployeeController(EmployeeRepository employeeRepository,
                       ReadEmployeeRepository readEmployeeRepository) {
        this.employeeRepository = employeeRepository;
        this.readEmployeeRepository = readEmployeeRepository;
    }

    @PostMapping("/employee")
    Employee create(@RequestBody Employee employee) {
        return employeeRepository.save(employee);
    }

    @GetMapping("/employee/read")
    List<Employee> readFromReplica() {
        return readEmployeeRepository.findAll();
    }
}

Choose the read repository only when the caller can accept replica consistency. A read immediately following a write may need the primary repository instead.

Repository routing is different from transaction read-only mode

Spring Data JPA’s current transactionality reference (identified as Spring Data JPA 4.1.1 and accessed September 30, 2026) states that inherited CRUD read operations default to readOnly=true, while declared query methods receive no transaction configuration automatically. The readOnly flag is propagated as a JDBC hint and may enable provider optimizations; it is not a routing switch and is not a guarantee that a manipulating query will be rejected.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Declare transaction boundaries for each unit of work and select the appropriate transaction manager when more than one is present. A read transaction on the primary manager still uses the primary database; it does not move that repository to the replica.

Understand replica staleness before exposing the endpoint

The tutorial demonstrates that, after employees are added through the primary repository, the read repository can still return the older set. That is an illustration of asynchronous replication, not a measured lag value or a universal waiting period. No lag measurement or built-in wait mechanism is established by the tutorial.

  • Use the primary repository for read-after-write screens, confirmation responses, and workflows that require the just-committed value.
  • Use the read repository for dashboards, searches and other requests where slightly old data is acceptable.
  • If your business rule requires monotonic or read-your-writes behavior, implement an explicit consistency strategy rather than assuming the replica has caught up.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Failure modes and checks

Both repository types resolve to the same database

Check that the read scan references readEntityManagerFactory, that the read factory uses spring.datasource.readUrl, and that each transaction annotation selects the intended transaction manager.

A read repository is missing or duplicated

Inspect package boundaries and filters. The primary scan must exclude ReadOnlyRepository; the read scan must include it. Avoid registering a third scan that sees the same interface without a deliberate reason.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Writes appear possible through the read path

Confirm that the read interface does not extend CrudRepository or expose mutation methods. Also verify database credentials and grants: an interface-level omission is an API design choice, not proof of write denial.

Startup fails with an ambiguous bean

Use distinct bean names and references for both factories and transaction managers, mark only the intended primary beans with @Primary, and qualify injections where Spring cannot infer the correct persistence unit.

Reads show old data

Assume replication lag first. Compare the request’s consistency requirement with the repository it calls; route consistency-critical reads to the primary rather than adding an unsupported fixed sleep.

Deployment checklist

  1. Define the read-only repository interface without mutation methods.
  2. Create and runtime-retain @ReadOnlyRepository.
  3. Annotate only repositories safe for replica reads.
  4. Configure the primary scan with an exclusion filter and the primary factory references.
  5. Configure the read scan with an inclusion filter and the read factory references.
  6. Create separate data sources, EntityManagers and transaction managers; mark the primary set appropriately.
  7. Set and secure the read URL, credentials and database permissions.
  8. Test repository selection, startup wiring, failover behavior and read-after-write expectations on the exact framework versions you deploy.

What this pattern does—and does not do

It gives you explicit, reviewable routing: ordinary repositories remain on the primary EntityManager, while marked repositories use a secondary EntityManager backed by the read URL. It does not provide automatic query classification, a replica-health check, a quantified lag guarantee, or database-enforced immutability. Those concerns require separate transaction design, operational monitoring and database security.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.