What Are You Delegating?
Every abstraction is a trade: it removes work by making decisions for you. Those decisions are still being made — just not by you, and not visibly. For each abstraction: what it genuinely handles, what remains yours, and the escape hatch.
An ORMAn HTTP client or SDKA managed database or cloud platformAn identity providerAn LLM or coding agentA web frameworkA library functionA managed message queueA cacheA container runtime and orchestrator
What are you delegating to an orm?
DatabaseYou write
1const user = await userRepository.findByEmail(email)The abstraction handles
- ✓SQL generation for your dialect
- ✓Parameter binding — which is what makes it injection-safe by default
- ✓Object ↔ row mapping and type coercion
- ✓Connection acquisition and release from the pool
- ✓Change tracking, so
save()writes only what changed
Still your responsibility
- →Query behaviour — what SQL this actually produces, and whether it is one statement or fifty
- →Index requirements — the ORM cannot create the index
findByEmailneeds; it will happily scan without it - →Transaction boundaries — where the transaction starts, ends, and what isolation level you got by default
- →N+1 risks — lazy loading turns attribute access into a query, invisibly, inside loops
- →Data consistency — cascades, orphans, and what a partial failure leaves behind
Know your escape hatch
When: The generated query is slow, the plan is wrong, or the shape of the read does not match any entity.
Drop to: Raw SQL for that query, and EXPLAIN to read what the database decided.
Keeping the ORM for 95% of the code and writing three hand-tuned queries is the normal outcome, not a failure of the ORM.