DEV Community

Odinaka Robert Nnamani
Odinaka Robert Nnamani

Posted on

All DB access lives in one place

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:

  1. Cleaner code — business logic stops drowning in query syntax.
  2. Safer schema changes — rename a column in one file, not seventeen.
  3. 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 (17)

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025
 
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!

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025
 
robertpaschal profile image
Odinaka Robert Nnamani •

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

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025
 
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.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025 Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

what breaks first when the outcome method gets big?

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025 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.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025 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.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025 Thread
 
alexcodebytes profile image
Oleksandr •

100%. The 'small rooms' analogy is perfect. It’s the classic microservices vs monolith debate. Splitting into sub-domains looks great on a diagram, but in practice, it just means you're now managing distributed transactions. Sometimes one well-organized warehouse is better than three disconnected rooms.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025 Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

small rooms is a good way to put it. the domain split is the one that actually holds.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025 Thread
 
alexcodebytes profile image
Oleksandr •

Exactly. The single access point is just the front door. If all the rooms behind it are one giant open warehouse, you've solved nothing. Domain boundaries are what make the 'one place' maintainable.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025 Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

That warehouse image sticks. A tidy front door means little if everything behind it can still wander anywhere.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025 Thread
 
alexcodebytes profile image
Oleksandr •

The 'wander anywhere' part is the real problem. A single access point without enforcement is just a suggestion. You need the door and the walls, or the door is just decoration.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025 Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

Door without walls is just a suggestion, yeah. The enforcement is the part the boundaries piece has to land on.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025 Thread
 
alexcodebytes profile image
Oleksandr •

Exactly. The door is the policy; the walls are the enforcement. And enforcement is where most 'single access point' architectures fail — they build the door, document the rule, and then trust everyone to follow it. Trust is not a boundary.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025 Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

a documented rule nobody enforces is just a suggestion with better formatting.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025
 
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.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025
 
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.

📢 แจ้งเตือน: robertpaschal/all db access lives in one place dj - Kapook Update 2025
 
robertpaschal profile image
Odinaka Robert Nnamani •

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