Skip to content

Readonly

WARNING

This is our own implementation of readonly! For this reason you should always import it with @wisemen/nestjs-typeorm.

Default repositories must be injected with InjectRepository.

Custom repositories must extend from TypeOrmRepository.

Readonly contexts let you route database reads to configured read replicas. Use them to offload read-heavy workloads from the primary database while accepting eventual consistency.

Readonly replicas are not included in our default project setup! So if you would like to actually start offloading reads, you should ask sysops to set this up for you.

INFO

Even though the readonly connections might not be set up, we still build our applications using the readonly helper when possible. This allows us to simply enable the connection, and have our entire application correctly offload reads where possible.

Why Readonly?

  • Offload read-heavy endpoints to replicas to improve scalability.
  • Keep the primary free for writes and critical transactions.
  • Accept that very recent writes may not be visible immediately due to replication lag.

Using Readonly

Use the readonly() helper to run a callback where queries are executed against a replica connection. Internally, this uses AsyncLocalStorage to provide the correct EntityManager to repositories.

typescript
import { InjectRepository, readonly } from "@wisemen/nestjs-typeorm";

export class ListActiveUsersUseCase {
  constructor(
    private readonly dataSource: DataSource,
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  async execute(): Promise<User[]> {
    return readonly(this.dataSource, async () => {
      return this.userRepository.find({ where: { isActive: true } });
    });
  }
}

Nested readonly scopes reuse the same readonly manager; no extra query runners are created.

Mutual Exclusivity

Readonly and transaction scopes cannot be mixed:

  • Starting a transaction inside a readonly scope throws Error('Cannot start a transaction inside a readonly context').
  • Starting a readonly scope inside a transaction throws Error('Cannot start a readonly context inside a transaction').
  • Calling EntityManager.transaction(...) while in a readonly scope throws Error('Cannot call EntityManager.transaction inside a readonly context').

See Transactions for details on starting and using transactions.

When to Use

  • GET/list/search endpoints that do not modify data.
  • Background jobs that read large datasets.
  • Any non-critical reads that can tolerate slight replication lag.

Caveats

  • Replication lag may hide recent writes on replicas for a short time.
  • Do not perform writes inside a readonly scope; replicas will reject writes.
  • Keep the callback focused on DB reads; avoid long-running or blocking operations.