DEV Community

All DB access lives in one place

Odinaka Robert Nnamani on October 26, 2026

One mistake I see in backend codebases: ORM queries scattered everywhere. Controllers query the DB. Services query the DB. Helpers query the DB. ...
Collapse
 
alexcodebytes profile image
Oleksandr •

The "no exceptions for a small helper" rule is where 90% of teams fail. The moment someone writes db.select() in a random utility file because "it's just one quick call," the repository abstraction is dead.

One thing I'd add: keeping the transaction boundaries inside the repository is a massive win for data integrity. If a controller has to orchestrate a multi-table transaction by calling three different repository methods, it usually ends in partial writes and corrupted state. Exposing a single createOrderWithItems() method that handles the transaction internally is a much cleaner design.

Great series idea!

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

so the question is where the boundary lives, inside or outside the repo?

Collapse
 
alexcodebytes profile image
Oleksandr •

It lives outside the repository, but inside the data access layer.

The common mistake is putting transaction logic in the controller or service (e.g., calling repo.begin(), then repo.saveA(), then repo.saveB()). If the controller controls the transaction, it's orchestrating DB logic, which breaks the abstraction.

Instead, the repository should expose an outcome-level method like repo.createUserWithProfile(userData, profileData). The method accepts the domain objects, opens the transaction, handles the commit/rollback, and closes it. The controller just calls that one method and doesn't even know a transaction is happening.

The boundary is the method signature: if it accepts domain data and performs a complete action, it belongs in the repo.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

what breaks first when the outcome method gets big?

Thread Thread
 
alexcodebytes profile image
Oleksandr •

Testability and developer friction. When an outcome method like createUserWithProfile() starts doing 15 different things (inserting 5 tables, sending emails, updating counters), it becomes impossible to mock, and any small change breaks 10 unrelated tests.

The fix is usually to break it down by sub-domain, not by table. Instead of one giant checkout() method, you have reserveInventory(), chargeCard(), and fulfillOrder() — each wrapped in its own transaction. The repository handles the data, but the orchestration moves up to a domain service level.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

The sub-domain split is the part I kept rereading. reserveInventory, chargeCard, fulfillOrder reads like three small rooms instead of one warehouse.

Collapse
 
arslan_ah profile image
Arslan Ahmad •

Agree with the rule, especially "no exceptions for the small helper." That's always where it leaks first. The underrated benefit is that once all access goes through one module, you can actually see your query patterns. Missing indexes, N+1s and transaction boundaries show up in one place instead of being discovered in a profiler. The trap on the other side is a repository that just wraps every ORM method one-to-one. Methods named for what the business needs are what make it worth it.

Collapse
 
williamsj04 profile image
Jessica Williams •

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.

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

Good catch, that's the piece #6 left open. The boundary question is where the next one goes.

Collapse
 
ihssmaheel profile image
Mohamed Ismail •

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.