Why Frameworks Should Provide Fewer Primitives

Framework design philosophy shows up in your codebase long before it shows up in your README. When a framework offers too many primitives, teams don’t gain flexibility — they gain inconsistency, debate, and slow erosion of clarity. This article explains why fewer, stronger primitives lead to systems that are easier to reason about, safer to change, and faster to ship over time.

Intro

There’s a moment a lot of teams hit that’s hard to explain at first.

You picked a solid framework.
You followed the docs.
You hired good engineers.

And yet, six months in, the codebase feels… uneven.

Not broken. Not slow. Just inconsistent.

Same problem solved three different ways.
Code reviews arguing about style instead of behavior.
New engineers asking, “Which pattern should I use here?”

This article is about why that happens.

And why, counterintuitively, the best frameworks don’t give you more building blocks. They give you fewer.


Why does our codebase feel inconsistent even though we use one framework?

This catches teams off guard.

You standardized. Everyone’s using the same stack. The same libraries. The same framework.

So why does everything look different?

The answer is uncomfortable but simple:

The framework gave you too many valid ways to do the same thing.

Different engineers made different “reasonable” choices.
All of them supported.
All of them idiomatic.

And now the system has multiple personalities.

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


More primitives don’t give you power — they give you choices

Frameworks often sell primitives as power.

“Look how flexible this is.”
“Look how many ways you can customize behavior.”
“You’re not locked in.”

What they don’t say is that every primitive is a decision you now have to make — over and over.

Which abstraction do we use here?
Which hook?
Which lifecycle?
Which pattern is “preferred”?

Those decisions feel cheap early. They’re expensive later.

Not because any single choice is wrong, but because consistency becomes optional.


Every primitive is a fork in your architecture

Think about it this way.

Every time a framework offers two equally valid ways to do something, it creates a fork.

Early on, nobody notices.
Later, those forks compound.

You don’t just have:

  • one way to handle auth
  • one way to run background jobs
  • one way to access data

You have families of patterns.

And once those patterns exist in production, they’re almost impossible to remove without pain.

Frameworks don’t just enable architecture.
They multiply it.


Flexibility feels good until it shows up in code review

This is where the cost becomes visible.

Code reviews start sounding like this:

  • “Why did you do it this way instead of that way?”
  • “We usually use the other pattern.”
  • “Both are fine, but…”

Both are fine.

That’s the problem.

Now reviews aren’t about behavior, correctness, or impact. They’re about preference. Taste. History.

Velocity drops, not because the work is hard, but because the system doesn’t push back.

Everything is allowed. Nothing is obvious.


Frameworks don’t just enable code — they shape thinking

This is the part framework designers often underestimate.

Engineers learn what’s acceptable by what the framework makes easy.

If a framework offers five ways to do something, engineers will assume all five are valid long-term options.

They won’t stop and ask:

  • “Which one should we standardize on?”
  • “Which one should never be used?”

Because the framework didn’t take a position.

So the team absorbs the ambiguity.
And ambiguity turns into divergence.


Auth is where primitive overload becomes obvious

Auth is usually the first place this hurts.

How it starts

Everything looks reasonable:

  • middleware checks permissions
  • helpers enforce rules
  • frontend hides buttons
  • jobs assume access is valid

Each of these uses a different primitive.
Each one feels appropriate in isolation.

The moment it breaks

Then you add:

  • impersonation
  • per-tenant permissions
  • background jobs acting on behalf of users

Now the question isn’t “how do we check auth?”

It’s “where is auth decided?”

The symptoms

You start seeing:

  • inconsistent access behavior
  • security fixes touching many files
  • fear around making auth changes
  • debates about the “right” pattern

Nothing is technically wrong.

But everything is fragile.

What fixes it

You reduce primitives.

One way to decide authorization.
Everything else asks.

No helpers making decisions.
No frontend guesses.
No job-level shortcuts.

Security stabilizes, not because you added checks, but because you removed choice.


Background jobs expose the cost of optional patterns

Async systems amplify primitive sprawl fast.

The naive setup

The framework offers:

  • multiple ways to enqueue jobs
  • different retry semantics
  • various lifecycle hooks

So teams use them.

The moment it breaks

Jobs start:

  • retrying after partial failure
  • mutating shared state
  • running out of order

Each job was built “correctly” according to the framework.

Collectively, it’s a mess.

The symptoms

You see:

  • duplicated side effects
  • “impossible” states
  • fear of retries
  • debugging sessions nobody enjoys

What fixes it

You collapse primitives.

One job abstraction.
One retry model.
One way jobs interact with state.

Less power.
More predictability.

Async stops being scary because it stops being surprising.


Repositories rot fastest when escape hatches are cheap

Data access is another classic trap.

How it starts

You introduce repositories.

But the framework also allows:

  • direct DB access
  • query helpers
  • raw SQL “just this once”

It’s flexible. Convenient.

The moment it breaks

Business rules start leaking.

One path enforces invariants.
Another bypasses them.

Now data behavior depends on which primitive you used.

The symptoms

  • subtle data inconsistencies
  • refactors touching everything
  • engineers afraid to change schemas
  • bugs showing up far from the change

What fixes it

You remove escape hatches.

Repositories become the only data primitive.
Exceptions are explicit, rare, and visible.

Suddenly, refactors get smaller.
Bugs get local again.


Observability suffers when primitives multiply

This one sneaks up on teams.

Frameworks often provide:

  • logging helpers
  • metrics hooks
  • tracing APIs
  • custom instrumentation points

Engineers pick different ones in different places.

Now there’s no single story of what happened.

During incidents, you don’t just debug the system.
You debug how people chose to observe it.

Fewer primitives means fewer narratives.

Observability improves not because tooling is better, but because behavior is more uniform.


“We’ll standardize later” is a lie

This is worth saying plainly.

Later almost never comes.

Once a primitive is used in production:

  • it has users
  • it has defenders
  • it has edge cases

Removing it costs more than adding it ever did.

Frameworks that lead with “you can do anything” quietly lock teams into everything.

Restraint up front is cheaper than cleanup later.


What restraint actually buys you

Frameworks with fewer primitives feel restrictive at first.

That’s intentional.

What you get in return:

  • fewer decisions
  • clearer defaults
  • faster onboarding
  • more consistent systems
  • code reviews about behavior, not taste

Speed doesn’t come from options.
It comes from rails.


Why SaasEasy deliberately says no

This is where SaasEasy is opinionated on purpose.

It doesn’t try to give you every possible primitive.
It gives you one good way to do the important things.

  • one way to handle auth decisions
  • one way to run workflows
  • one way to access data
  • one way to observe behavior

Not because alternatives are wrong.

But because allowing them would make the system harder to reason about six months from now.

SaasEasy trades theoretical flexibility for practical velocity.


Frameworks should say no

A good framework doesn’t ask:

“What can we support?”

It asks:

“What should teams not have to think about?”

Every primitive you remove is a decision your users never have to make.
Every opinion you encode is a debate they never have to have.

Fewer primitives isn’t limitation.

It’s respect for the fact that real systems are built by humans, under pressure, over time.

And humans move faster when the path is obvious.

Scroll to Top