ORM sucks. What are your options?
Every ORM sells the same pitch: you write objects, it writes SQL, and you never have to think about your database again. That last part is the problem. You always have to think about your database. The ORM just makes it harder.
After 25 years of building software — from enterprise finance platforms to real-time IoT dashboards — I have used every major ORM across multiple languages. Hibernate, Entity Framework, SQLAlchemy, Sequelize, TypeORM, Prisma. The pattern is always the same. It starts elegant. Months later, your team is fighting the ORM instead of solving business problems.
The fundamental mismatch
The original sin of ORMs has a name: object–relational impedance mismatch. Objects have identity, encapsulation, inheritance, and polymorphism. Relational tables have rows, columns, foreign keys, and set operations. These two models do not map cleanly onto each other — and every ORM is an elaborate attempt to pretend they do.
SQL is grounded in relational algebra — projection, selection, joins, set operations. Its power comes from that mathematical foundation, not from object-oriented thinking. Forcing it through an object graph means the ORM must constantly translate between two incompatible worldviews. Lazy loading, eager loading, N+1 problems, cascade behaviours, identity maps — these are not incidental bugs. They are structural consequences of trying to bridge a gap that should not be bridged.
When you let SQL be SQL, entire categories of ORM complexity simply vanish.
The abstraction that leaks on schedule
ORMs are the textbook leaky abstraction. They work beautifully for CRUD operations that you could have written in five lines of SQL. The moment you need a complex join, a window function, a recursive CTE, or a query that actually performs well at scale — the ORM fights you.
You end up writing ORM-flavoured pseudo-SQL that is harder to read than actual SQL, harder to optimise, and impossible to debug without dropping down to raw queries anyway. The abstraction that was supposed to save you time now costs you time, plus the overhead of understanding what the ORM generates behind your back.
And it gets worse. Most ORMs do not stop at mapping — they introduce their own query language. Hibernate has HQL and the Criteria API. Doctrine has DQL. Entity Framework has LINQ-to-Entities. Each is a proprietary DSL that looks almost like SQL but is not SQL. So instead of learning one well-designed, universally portable query language, your team learns a vendor-specific dialect that works nowhere else, has worse tooling, and still falls back to raw SQL the moment things get serious.
That is the real cost: not just an abstraction layer, but an entirely new language to learn, debug, and maintain — one that exists solely to avoid the language your database already speaks fluently.
There is a deeper issue. Developers who grow up inside an ORM often never learn SQL properly. They can define models and call .findMany(), but hand them an execution plan and they freeze. The ORM did not just abstract the database — it abstracted away the knowledge needed to use it well.
The epistemological trap
Every abstraction layer is a bet: the cost of not understanding what’s underneath is lower than the cost of dealing with it directly. For UI frameworks, that bet usually pays off. For databases — the system that determines whether your application responds in 20 milliseconds or 20 seconds — it fails more often than ORM vendors admit.
Knowing your database is not optional complexity. It is essential knowledge. An ORM that hides it does not simplify your system. It delays the moment when complexity surfaces, and guarantees you are less equipped to handle it when it does.
So what are the options?
Raw SQL with type safety
Tools like sqlc (Go) or PgTyped (TypeScript) let you write plain SQL and generate type-safe code from it. You get full control over your queries, the database’s query planner sees exactly what you wrote, and your IDE still catches type errors at compile time.
This is the approach I reach for most often. SQL is already a declarative, well-designed language. Wrapping it in another abstraction layer rarely improves it.
Query builders
Kysely (TypeScript), Knex, and JOOQ (Java) sit in the middle ground. They provide composable, type-safe query construction without pretending your database is an object graph. You still think in tables, joins, and conditions — but with the safety net of your type system.
Query builders shine when you need dynamic queries — filters, pagination, conditional joins — where raw SQL gets unwieldy with string concatenation. They compose cleanly without hiding what they produce.
Thin mapping layers
Not every alternative requires going fully raw. Libraries like Drizzle ORM take an SQL-first approach: the API mirrors SQL semantics instead of inventing its own object model. You define schemas, but queries read like SQL, and you can drop to raw queries without friction.
The key distinction: thin mappers help you write SQL. Traditional ORMs try to replace SQL. The first approach scales. The second breaks.
The AI angle changes the equation
Here is what makes this conversation particularly timely. The strongest argument for ORMs was always developer convenience — SQL is verbose, error-prone, and most engineers would rather not write it by hand. That argument is evaporating.
AI agents write SQL fluently — and they are significantly better at SQL than at ORM code. The reason is straightforward: the internet contains orders of magnitude more raw SQL than Hibernate Criteria API calls or Prisma query syntax. SQL is universal. Every database documents it. Every Stack Overflow answer uses it. Every textbook teaches it. ORM DSLs are fragmented across vendors and versions, with far less training data and far more ambiguity in their semantics.
At Interlusion, agents generate queries, optimise them against execution plans, and produce type-safe database access code as part of the normal development workflow. They do this better and faster with raw SQL than with any ORM abstraction, because there is less indirection between intent and output. The convenience gap between “write an ORM call” and “write actual SQL” has not just collapsed — it has inverted. SQL is now the easier path.
This shifts the calculus entirely. If the only remaining benefit of an ORM is saving keystrokes, and an agent eliminates that benefit, what is left? A runtime abstraction layer that generates unpredictable queries, adds latency, increases bundle size, and prevents your team from understanding the system they depend on.
The pragmatic answer: use the database directly, let agents handle the boilerplate, and invest the time you saved into understanding your data model. That understanding compounds. ORM convenience does not.
Stop defaulting to the ORM
The default should not be “add an ORM”. The default should be “use SQL, and only add abstraction when the complexity of your query composition genuinely demands it”.
The industry spent two decades normalising the idea that SQL is too hard for application developers. It is not. It is a skill, like any other. And it is a skill that pays dividends every time a production query runs slowly at 3 AM and someone needs to understand why.
Stop hiding from your database. It is the most important part of your system.
Building something data-intensive and want to get the architecture right from the start? Let’s talk.