One mistake I see in backend codebases:
ORM queries scattered everywhere.
Controllers query the DB. Services query the DB. Helpers query the DB. The little cron job queries the DB. Change one column and you're grep-ing the whole repo, praying you found every query that touches it.
Soon your database logic is impossible to track — and nobody can tell you where a value actually gets read or written.
A better pattern: the repository layer.
All DB access lives in one place. Controllers ask the repository; they never touch the ORM. Schema knowledge — joins, filters, transaction boundaries — lives in one module per entity. The rest of the codebase just calls methods with names that say what they mean: findActiveUsers(), not db.select().where(...).
Three things this buys you:
- Cleaner code — business logic stops drowning in query syntax.
- Safer schema changes — rename a column in one file, not seventeen.
- A clean test seam — the boundary you need to mock is obvious.
The rule I keep: no raw ORM calls outside the repository. No exceptions for "it's just a small helper." Break the rule once and it's no longer a rule.
All DB access lives in one place. That's the whole point.
Next in this series: debugging a distributed mystery. I write about backend systems that look small until they aren't — follow along if that's your world; the longer pieces live on my Substack.
Top comments (2)
Great post! I'm following the same approach in my projects. I think the repository pattern is pretty common in modern backend development now, but it's still surprising how often database queries end up scattered across the codebase. Keeping everything in one place makes maintenance so much easier.
The seam for mocking is a real benefit. One thing I would add is how transaction boundaries are handled once a single business action touches more than one entity, for example an order that changes stock and also writes an audit row. If each repository opens its own transaction, those writes can commit separately. Passing a transaction handle, or a small unit-of-work object, into each repository method keeps one commit for the whole action while the queries still live in one place. It also lets a test assert that nothing was written when the second step fails.