6. Design Rationale
6.1 Why This Section Exists
This section documents alternatives considered during the design of the query-layer migration and explains why each was rejected. Each alternative is evaluated against the invariants defined in Section 2.3.
6.2 Rejected Alternatives
6.2.1 Remain on Prisma Client
Description: Keep Prisma Client as the runtime query generator for all tenant SQL operations and address performance concerns through Prisma-level optimizations (query batching, selective includes, raw SQL for hot paths).
Why It Was Attractive:
- Avoids migration effort and coexistence complexity entirely.
- Preserves the existing developer workflow and Prisma-generated types.
- Prisma's ecosystem continues to evolve with potential performance improvements in future versions.
Why It Was Rejected:
- The Prisma Engine binary interposes between the application and the database, adding serialization and processing overhead that is inherent to the architecture and cannot be eliminated through query-level optimization.
- Benchmark evidence demonstrates a 46.9% latency reduction on a representative read endpoint when the Prisma Engine is bypassed.
- Prisma Client does not support per-query schema scoping, requiring
continued reliance on
search_pathmutation for multi-tenant isolation. - The per-tenant client caching model creates linear connection growth that cannot be bounded without architectural change.
- Escaping to
$queryRawfor performance-sensitive paths forfeits Prisma's type safety, negating the primary benefit of the ORM.
Conclusion: Remaining on Prisma Client preserves the root causes of query-layer overhead and multi-tenant schema risk. Optimization within Prisma's constraints cannot achieve the overhead reduction or schema safety that a query builder migration provides.
6.2.2 Drizzle ORM
Description: Adopt Drizzle ORM as the replacement query builder instead of Kysely. Drizzle provides a TypeScript-first SQL query builder with schema definition, relational queries, and migration tooling.
Why It Was Attractive:
- Drizzle provides an ORM-like relational query API that reduces the manual effort of relation hydration compared to Kysely.
- Drizzle's schema definition can serve as both query type source and migration definition, potentially consolidating tooling.
- Growing community adoption and active development.
Why It Was Rejected:
- Drizzle's
withSchema()support for PostgreSQL schema-per-tenant patterns is less mature than Kysely's. Schema scoping behavior has had documented issues in multi-schema environments. - Drizzle's relational query API, while convenient, adds an abstraction
layer with its own query generation logic, reintroducing a category
of opacity similar to Prisma's
includedirective. - Adopting Drizzle's schema definition would require maintaining a second schema source alongside the existing Prisma schema, creating drift risk. Using Drizzle without its schema definition reduces it to a query builder with a less proven multi-tenant story than Kysely.
- The platform already has a working Kysely prototype with benchmark evidence. Drizzle would require a new prototype and validation cycle.
- Violates the risk profile implied by Invariant 2 (Multi-Tenant Isolation) given the less mature schema scoping.
Conclusion: Drizzle ORM introduces schema-scoping maturity risk and abstraction opacity that Kysely avoids. The existing Kysely prototype and benchmark evidence provide a validated starting point that Drizzle does not offer.
6.2.3 Raw SQL via pg Driver
Description: Replace Prisma Client with raw SQL strings executed
directly through the pg driver, without a query builder abstraction.
Why It Was Attractive:
- Eliminates all query builder overhead; SQL strings are sent directly to the database with zero transformation.
- Provides complete control over every aspect of the generated SQL.
- Simpler dependency footprint with no query builder library.
Why It Was Rejected:
- Raw SQL strings have no compile-time type safety for column references, parameter types, or result shapes.
- SQL injection risk increases when query composition involves string concatenation for dynamic filters, sorts, and pagination.
- Maintenance burden scales with endpoint count; each query is an opaque string rather than a composable, typed expression.
- Refactoring is error-prone because column and table name references are embedded in strings rather than typed identifiers.
- Violates Invariant 1 (Type Safety).
Conclusion: Raw SQL sacrifices type safety and maintainability for marginal performance gains over a typed query builder. The risk of runtime errors and SQL injection outweighs the benefit of eliminating the query builder layer.
6.2.4 TypeORM
Description: Adopt TypeORM as the replacement query builder. TypeORM is a mature TypeScript ORM with decorator-based entity definitions, a query builder API, and migration tooling.
Why It Was Attractive:
- Mature ecosystem with long track record in TypeScript projects.
- Decorator-based entity definitions integrate with NestJS patterns.
- Built-in migration tooling could consolidate with or replace Prisma Migrate.
- Active Record and Data Mapper patterns provide flexible usage modes.
Why It Was Rejected:
- TypeORM's runtime overhead is comparable to Prisma's for complex queries. The decorator-based entity system and query builder add abstraction layers that reintroduce the category of overhead this migration aims to eliminate.
- TypeORM's query builder generates SQL through its own internal planner, reducing query transparency similarly to Prisma.
- Schema-per-tenant support in TypeORM requires custom connection
management or dynamic schema injection, which does not provide the
per-query
withSchema()ergonomics that Kysely offers. - Adopting TypeORM would require defining entities with decorators alongside the existing Prisma schema, creating a dual-schema maintenance burden.
- TypeORM has documented performance and stability concerns in high-concurrency environments.
Conclusion: TypeORM reintroduces the runtime overhead and query opacity that motivate this migration. Its schema-per-tenant support lacks the per-query scoping guarantees required by Invariant 2.