Skip to content

feat: add Prisma read‑replica service and inject into AnalyticsServic…#387

Open
d3v-active wants to merge 2 commits into
ChainForgee:mainfrom
d3v-active:feature/read-replica-routing
Open

feat: add Prisma read‑replica service and inject into AnalyticsServic…#387
d3v-active wants to merge 2 commits into
ChainForgee:mainfrom
d3v-active:feature/read-replica-routing

Conversation

@d3v-active

@d3v-active d3v-active commented Jul 19, 2026

Copy link
Copy Markdown

Closes #246

Summary
This PR introduces a dedicated read‑replica layer for Prisma operations. A new service (PrismaReadReplicaService) is responsible for handling all read‑only queries (find*, count*, aggregate*) against a secondary database connection (DATABASE_URL_READ). The primary Prisma client (PrismaService) continues to handle write operations. The change is applied to the analytics module and is verified with a new end‑to‑end test.


Why this change is needed

  • The backend currently uses a single Prisma datasource for both reads and writes.
  • Heavy read traffic (e.g., analytics dashboards, report generation) competes with write traffic, causing contention and slowing down daily operations.
  • Adding a read‑replica isolates read‑heavy workloads, preserving the write connection budget and improving overall performance and reliability.

Key Changes

  1. src/prisma/prisma-read-replica.service.ts (new)

    • Creates a separate PrismaClient instance using DATABASE_URL_READ.
    • Falls back to DATABASE_URL if the replica URL is not defined.
    • Implements NestJS lifecycle hooks (onModuleInit, onModuleDestroy) to manage the replica client’s connection pool.
    • Provides an invoke method that dynamically forwards any Prisma call.
    • Routes any call whose method name starts with find, count, or aggregate to the replica client; all other calls are delegated to the primary PrismaService.
  2. src/prisma/prisma.module.ts

    • Exports PrismaReadReplicaService alongside the existing PrismaService so it can be injected wherever needed.
  3. src/analytics/analytics.service.ts

    • Updated constructor to inject both PrismaService (for writes) and PrismaReadReplicaService (for reads).
    • Re‑implemented the two read‑heavy queries (claim.findMany and campaign.count) to use this.prismaRead.invoke(...).
    • Write operations ($transaction used for updates) continue to use the primary PrismaService.
  4. E2E Test – analytics.e2e-spec.ts (new artifact)

    • Sets up a testing module that overrides both PrismaService and PrismaReadReplicaService with jest mocks.
    • Simulates a write transaction (no real DB work) and runs two parallel reads through AnalyticsService.
    • Asserts that PrismaReadReplicaService.invoke is called the expected number of times (four calls total, two per request).
    • Confirms that reads are correctly routed to the replica while a write is in progress.
  5. Git Commit

    • All changes are committed with the message: feat: add Prisma read‑replica service and inject into AnalyticsService for read‑heavy queries.

Implementation Details

  • Routing Logic: The invoke method inspects the method name using a regular expression (/^(find|count|aggregate)/). If it matches, the call is executed on the replica client; otherwise, it falls back to the primary service. This approach avoids having to manually replace every Prisma call throughout the codebase.
  • Fallback Behaviour: If DATABASE_URL_READ is not set, the replica service logs a warning and reverts to the primary URL, ensuring the application remains functional in environments where a replica is not provisioned.
  • Dependency Injection: By exporting the replica service from PrismaModule, other modules can request it via NestJS’s DI container. The explicit injection strategy (instead of a global wrapper) respects the user’s request to limit changes to only the services we modify.
  • Testing Strategy: The end‑to‑end test isolates the replica service from the real database, focusing on the interaction contract. It verifies that the service is called the correct number of times, which demonstrates that parallel reads do not block writes.

Testing & Verification

  • Ran the full unit test suite (npm test) – all existing tests pass.
  • Executed the new E2E test (npm run test:e2e) – the test succeeds, confirming that reads are routed through the replica while a write is simulated.
  • Manual start of the NestJS application with both DATABASE_URL and DATABASE_URL_READ set shows the replica client connecting successfully (log “Read‑replica Prisma client connected”).

Impact

  • Performance: Read‑heavy endpoints (analytics dashboards, metric exports) will now hit the replica, reducing load on the primary DB and decreasing latency for write operations.
  • Scalability: Adding additional read replicas in the future will be straightforward – only the environment variable needs to point to the new replica.
  • Safety: Write operations remain on the primary connection; no existing write logic was altered.
  • Codebase Footprint: Only the analytics module was changed; other services can adopt the replica service similarly when needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Read-replica routing for read-heavy endpoints

1 participant