Why You Might Not Need an ORM (and When You Do)
Team 5 min read
#databases
#backend
#architecture
#orm
TL;DR
- You might not need an ORM if your data access is straightforward, performance-critical, or analytics-heavy, or if your team is fluent in SQL.
- An ORM shines when your domain model is complex, you need cross-database portability, or your team benefits from strong type-safety, composability, and tooling around migrations and transactions.
- There is a middle ground: query builders and micro-ORMs often deliver 80% of the benefits with less overhead.
What an ORM Actually Does
- Maps rows to in-memory objects or records (identity map, unit of work).
- Generates SQL for you from high-level constructs.
- Provides abstractions for relations, transactions, migrations, and sometimes schema tooling.
- Often adds validation, schema typing, and conventions that reduce boilerplate.
When You Probably Don’t Need an ORM
- Simple CRUD and reporting services
- Few tables, clear joins, limited business logic.
- A handful of parameterized queries can stay readable and fast.
- Performance-critical paths
- Raw SQL gives tighter control over projections, joins, and indexes.
- Easier to reason about query plans and avoid accidental N+1 fetches.
- Analytics, data warehousing, and complex SQL
- Window functions, CTEs, lateral joins, recursive queries, custom operators.
- ORMs often lag in expressiveness or produce suboptimal SQL for advanced features.
- Database-first environments
- DBA-managed schemas, stored procedures, triggers, and policies dominate.
- Application layer is a thin wrapper around well-defined SQL contracts.
- Small services with clear boundaries
- Short-lived utilities, cron jobs, ETL steps where SQL clarity trumps abstraction.
When an ORM Is Worth It
- Rich domain models and lifecycles
- Aggregates, invariants, and complex relationships where an identity map and unit-of-work reduce bugs.
- Large teams and long-lived codebases
- Consistency, discoverability, and conventions help reduce drift and onboarding time.
- Type-safety and productivity
- End-to-end typing of schemas, queries, and results reduces runtime errors.
- Database portability and testing
- Swapping vendors, using in-memory test databases, or mocking layers can be easier.
- Tooling ecosystem
- Migrations, schema diffing, seeding, and code generation tightly integrated.
Alternatives to a Full ORM
- Parameterized raw SQL
- Safest and most explicit. Combine with small helpers for mapping rows to types.
- Query builders
- Knex, Kysely, jOOQ, SQLDelight, SQLc, Diesel’s query DSL. Strong middle ground: expressive SQL, good typing, no heavy identity map.
- Micro-ORMs
- Dapper, Sqlx, PetaPoco. Thin mapping with minimal magic.
- Stored procedures and views
- Push complexity into the database when that’s the source of truth and performance is paramount.
Performance and Correctness Pitfalls (and Mitigations)
- N+1 queries
- Use eager loading, select-related/includes, or explicit JOINs. Project only fields you need.
- Over-fetching
- Prefer projections over fetching entire entities. Use DTOs or select clauses.
- Hidden transactions
- Be explicit about boundaries. Group operations logically and handle errors carefully.
- Impedance mismatch
- Don’t force object graphs where relational modeling is clearer. Embrace normalized schemas and constraints.
- Index and query plan blindness
- Profile with EXPLAIN/ANALYZE. Whether ORM or SQL, verify the plan and index coverage.
Security, Migrations, and Schema Evolution
- SQL injection
- Use parameterized queries everywhere. Most ORMs and query builders handle this well.
- Migrations
- Prefer migration files that you can review. Auto-generated diffs are helpful, but always inspect them.
- Constraints and data integrity
- Enforce invariants in the database with primary keys, foreign keys, checks, and unique constraints. Application checks are not enough.
Decision Checklist
- Data shape: Mostly CRUD with simple joins? Raw SQL or a query builder may suffice.
- Domain complexity: Rich aggregates and invariants? ORM can reduce boilerplate and errors.
- Team skills: Strong SQL fluency? Lean into SQL. Mixed experience? ORM or builder can standardize practices.
- Performance sensitivity: Hot paths and tight SLOs? Prefer explicit SQL for critical operations.
- Longevity and scale: Big team, multi-year product? ORM or query builder for consistency and tooling.
- Advanced SQL features: If you need heavy CTEs, window functions, custom types, or extensions, ensure your tool supports them natively.
Example Side-by-Side
Raw SQL (Node.js with pg):
import { Pool } from "pg";
const pool = new Pool();
async function listActiveUsers(orgId: string) {
const sql = `
select id, email, created_at
from users
where org_id = $1 and deleted_at is null
order by created_at desc
limit 100
`;
const { rows } = await pool.query(sql, [orgId]);
return rows; // Typed via zod/io-ts if needed
}
Query Builder (Kysely):
const users = await db
.selectFrom('users')
.select(['id', 'email', 'created_at'])
.where('org_id', '=', orgId)
.where('deleted_at', 'is', null)
.orderBy('created_at', 'desc')
.limit(100)
.execute();
Full ORM (Prisma):
const users = await prisma.user.findMany({
where: { orgId, deletedAt: null },
select: { id: true, email: true, createdAt: true },
orderBy: { createdAt: 'desc' },
take: 100,
});
Practical Guidance
- Start simple. Use raw SQL or a query builder. Measure if you need more.
- Add an ORM when modeling complexity, cross-cutting concerns, and team productivity justify it.
- Mix and match. Many teams use an ORM for most cases and drop to raw SQL for hot paths or complex reporting.
- Keep ownership in the database. Use constraints, indexes, and explicit transactions regardless of your abstraction.
Closing Thoughts You do not need an ORM to write safe, maintainable, and fast data access. ORMs are tools for managing complexity and standardizing workflows, not mandatory layers. Choose the simplest approach that meets your domain needs, and evolve deliberately as requirements grow.