Why Repositories Exist (and Why Most Are Wrong)

The SaaS repository pattern is one of those ideas everyone uses, few question, and most quietly get wrong. Repositories show up early in codebases, feel useful for a while, and then slowly fade into irrelevance as logic leaks everywhere else. This article explains why that happens, what repositories are actually for in real SaaS systems, and how to use them in a way that makes your system calmer instead of more fragile.

Intro

If repositories feel useless to you, you’re not wrong.

Most repos I see in real SaaS codebases are decorative. They exist because someone once said “clean architecture,” not because they actually solved a problem. They wrap an ORM, forward a few methods, and quietly rot.

Then six months later, the system is a mess and everyone is confused about how it got there.

This article is about why repositories exist at all — not in theory, not in blog-diagram-land, but in the systems we actually ship. And why the way most teams implement them guarantees disappointment.

I’m going to be opinionated. Not because there’s only one way to do this — there isn’t — but because there is a direction that hurts less over time.


If repositories feel pointless, that’s a signal

Let’s start with the uncomfortable part.

If your repositories feel like they add nothing:

  • You’re probably right.
  • And you probably didn’t screw up — you followed common advice.

Thin repos. No logic. Just data access. Push “business logic” up to services.

That advice sounds clean.
It also quietly destroys systems.

The result is usually:

  • Repos that are just ORM pass-throughs
  • Services that become junk drawers
  • Rules duplicated across handlers, jobs, scripts, and sync paths
  • Nobody knowing where behavior actually lives

If you’ve ever asked “why do we even have this repo?” — good. That question means you’re paying attention.


Why we added repositories in the first place

Nobody adds repositories because they woke up craving abstraction.

They add them because:

  • They’re afraid of locking into a database
  • Tutorials say “don’t couple to your ORM”
  • It feels more professional
  • A past system hurt and they want to avoid that pain again

All reasonable instincts.

The problem is what we think repositories are for.

Most teams believe repositories exist to:

  • Hide SQL
  • Swap databases later
  • Isolate persistence from logic

None of those are wrong.
They’re just not the main point.

And when you optimize repos for those goals, you miss the reason they actually matter.


“The ORM already talks to the database — so what’s the point?”

This is the moment where things usually fall apart.

You already have an ORM.
It already does CRUD.
It already knows the schema.
It already validates types.
It already feels “high level.”

So your repo becomes this:

getById(id)
create(data)
update(id, data)
delete(id)

Which is just the ORM, but with worse autocomplete.

At that point, yeah — the repo is pointless.

But that’s not because repositories are bad.
It’s because you aimed them at the wrong problem.

ORMS solve mechanical problems.
Repositories exist to solve behavioral ones.

If your repo only moves data, it will always feel redundant.
Because moving data is the least interesting thing your system does.


The day a simple change breaks three unrelated features

Every real SaaS system has this moment.

It usually starts small.

Someone adds:

  • a soft delete
  • a new visibility rule
  • a feature flag
  • a tenant boundary tweak
  • a “temporary” exception

They update the obvious paths.
Tests pass.
Everything looks fine.

Then a week later:

  • A background job does the wrong thing
  • An admin endpoint leaks data
  • A sync worker resurrects deleted rows
  • A customer sees something they shouldn’t

And everyone says the same thing:

“Wait… this shouldn’t be possible.”

That sentence is the sound of behavior leaking.


You don’t have data problems — you have behavior sprawl

Here’s the uncomfortable truth.

Most SaaS systems don’t fail because of bad schemas.
They fail because behavior gets smeared across the codebase.

You see the same rules implemented in:

  • API handlers
  • Background jobs
  • Webhooks
  • Admin tools
  • One-off scripts
  • “Just this once” fixes

Each one makes sense locally.
Collectively, they rot the system.

The database becomes the path of least resistance.
“Just query the table.”
“Just update the row.”
“We’ll handle the edge cases here.”

And now your system has no center of gravity.


Thin repositories aren’t clean — they’re evasive

The advice to keep repositories thin is everywhere.

It sounds responsible.
It sounds disciplined.
It’s usually wrong.

Thin repos don’t eliminate logic.
They just force it to live somewhere worse.

What happens instead:

  • Services balloon
  • Helpers multiply
  • Conditionals spread
  • Invariants become conventions
  • “Everyone just knows” how things work

This is how you end up with five ways to update the same entity.
All slightly different.
All “technically correct.”
All subtly broken.

That’s not separation of concerns.
That’s avoidance.


Example: Auth and multi-tenant leakage

This is the classic SaaS footgun.

The naive version

Early on, everything is simple.

You query like this:

SELECT * FROM users WHERE tenant_id = ?

Then you add:

  • admins
  • background jobs
  • internal tools
  • migrations
  • support scripts

Each one “just queries the table.”
Each one remembers to filter.
Until one doesn’t.

The moment it broke

A background job sends an email.
Wrong tenant.
Wrong data.
Real customer impact.

The symptom

Everyone says:

  • “How did that even happen?”
  • “Where do we fix this?”
  • “How many places do we need to audit?”

Nobody trusts the answer.

The fix

The repository owns tenant scoping.

  • Tenant filters are automatic
  • Unsafe access requires explicit opt-out
  • Raw queries are the exception, not the default

The rule stops being a convention.
It becomes a property of the system.


The real job of a repository

Repositories do not exist to hide SQL.

They exist to:

  • Centralize behavior
  • Enforce invariants
  • Collapse choice
  • Make wrong code harder to write

A good repository answers questions like:

  • “What does it mean to fetch this entity?”
  • “What rules always apply?”
  • “What transitions are allowed?”
  • “What context is required?”
  • “What can’t happen?”

If your repo doesn’t do that, it’s not finished.


Example: Background jobs behaving differently than APIs

This one shows up later.
And it hurts more.

The naive version

APIs do validation.
Jobs do updates.
Helpers are shared.
Everyone moves fast.

The moment it broke

A job flips a feature flag.
Users suddenly have access they shouldn’t.

The API path was safe.
The job path wasn’t.
Both were “correct” in isolation.

The symptom

  • “Works when I click the button”
  • “Fails overnight”
  • “We can’t reproduce it locally”

Classic.

The fix

The repository encodes allowed transitions.

Jobs don’t bypass rules.
They use the same entry points.
Behavior becomes uniform by construction.


If you don’t centralize behavior, the system will smear it everywhere

This isn’t a discipline problem.
It’s physics.

Engineers optimize locally.
They do the easiest thing that works.
Especially under pressure.

If the system allows five ways to do something,
all five will be used.

Repositories are friction.
Intentional friction.

They say:

“If you want to touch this data, you come through here.”

That constraint is not bureaucracy.
It’s how you keep systems understandable.


Why most teams realize this too late

Because early success lies to you.

  • Low traffic hides inconsistencies
  • Small teams share context
  • Everyone remembers the rules
  • Bugs are survivable

Then the system grows.
And nothing breaks loudly.
It just gets… weird.

By the time you notice:

  • The logic is everywhere
  • Refactors are scary
  • You’re afraid to centralize now

That’s when teams say:

“We should have done this earlier.”

They’re right.


Example: Observability that doesn’t explain anything

This one is subtle.

The naive version

Queries everywhere.
Logs everywhere.
Metrics everywhere.

None of it means anything.

The moment it broke

A slow query cascades.
Timeouts.
Pages failing.

Everyone stares at dashboards.

The symptom

  • “We see the query… but why?”
  • “Which feature caused this?”
  • “Who triggered it?”

Data without intent.

The fix

Repositories become observability boundaries.

Queries are wrapped with meaning.
Logs know why data changed.
Metrics align with behavior, not tables.

Debugging stops being archaeology.


What this means in practice

This is not about:

  • More layers
  • More abstractions
  • More ceremony

It’s about deciding where behavior lives.

Repositories should:

  • Be boring
  • Be opinionated
  • Reject bad calls
  • Encode defaults
  • Remove choice

If calling your repo feels restrictive, that’s good.
That’s the system helping you.


Why SaaSEasy cares about this

SaaSEasy is opinionated about repositories because we’ve seen this movie too many times.

Not because repos are trendy.
Not because architecture diagrams say so.

But because once behavior fragments, you don’t get it back cheaply.

Systems that survive are the ones that:

  • Decide early
  • Centralize rules
  • Resist entropy
  • Make the right thing obvious

Repositories are one of the few tools that actually help with that.


If your repository feels boring, you did it right

The best repositories don’t impress anyone.

They don’t show off.
They don’t expose flexibility.
They don’t invite creativity.

They quietly make the system smaller.

If your repo feels boring…
If it removes debate…
If it forces consistency…
If it makes bugs harder to write…

Congratulations.

That’s the point.

Scroll to Top