Go One Layer Deeper
One ordinary line of code, expanded downward. Every layer names what it hides for you, how it fails, and where to learn it properly. Descend as far as the problem requires — and stop.
1await userRepository.save(user)- 1
ORMs remove genuinely tedious work and are worth using. They also make the expensive operations look exactly like the cheap ones, which is why performance problems in ORM code are usually structural rather than local.
What SQL did that actually produce, and in which transaction?
What are you delegating to an orm? →- ORM↓
- N+1 queries↓
- Database load↓
- Latency↓
- Production incident
What are you delegating here?
1const user = await userRepository.findByEmail(email)- ✓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
- →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
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.