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.

What are you delegating to an orm?
Database
You 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 findByEmail needs; 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.