Blog post

When a SaaS Stops Being About Features

Why building a real multi-tenant SaaS stops being a feature problem and becomes a problem of trust boundaries, guarantees, and responsibility placement.

I thought I was solving a product problem.

I wanted an orders table.

That was the surface-level request: show the data, make the UI work, wire the flow, move on.

But once I started building a real SaaS instead of a demo, the question changed almost immediately.

It was no longer:

How do I show orders in the interface?

It became:

Who is allowed to see them, in which tenant, under which rules, and where does that logic actually belong?

That is the moment when software stops being mostly about features. That is the moment when it becomes about guarantees.

The illusion I started with

At the beginning, my mental model was still too close to application development in its most optimistic form.

I thought a well-structured stack would carry more of the architecture than it actually does:

  • Next.js for the app structure
  • Drizzle for database access
  • auth primitives for identity
  • some clean separation between routes and business logic

And to be fair, that setup can get you surprisingly far.

The problem is that "far enough to make the app work" and "far enough to make the system trustworthy" are not the same threshold.

That was the first uncomfortable realization.

Working code can still be architecturally wrong.

The real shift

The turning point was not a single abstraction or pattern. It was the moment I realized I was no longer mostly asking:

How do I implement this feature?

I was asking:

Where should this responsibility live so it cannot be misused?

That is a different kind of question.

It forces you to care about things that are easy to postpone when you are still thinking in feature mode:

  • trust boundaries
  • tenant scope
  • authorization placement
  • transaction guarantees
  • internal escape hatches

None of these are solved by a framework.

Frameworks give you speed. They do not give you guarantees.

Why multi-tenancy changes the game

In a single-tenant internal app, you can get away with many assumptions for longer than you should.

In a multi-tenant SaaS, those assumptions become liabilities very quickly.

The reason is simple:

tenant is not just a field. It is a trust boundary.

If the wrong company can see the wrong record, there is no "almost correct" version of that system.

And once you accept that, a lot of seemingly small design choices stop being small.

For example:

  • tenant scope cannot casually come from the UI
  • roles cannot be interpreted differently in different layers
  • dangerous paths cannot be protected only by convention
  • write flows that must stay coherent cannot be treated like unrelated operations

This is where architecture stops being taste. It becomes risk management.

This is also the point where names like Clean Architecture, Hexagonal Architecture, and DDD start becoming more than diagram vocabulary.

Before that stage, they are easy to treat as style preferences or as ways to make the codebase look more disciplined.

Once trust boundaries and misuse risks become real, their practical value changes.

Not because I suddenly need ideological purity around patterns. But because I need a more reliable way to answer questions like:

  • where does policy belong?
  • what should be allowed to depend on what?
  • which parts of the system should stay close to the domain instead of leaking infrastructure concerns?

That is the only version of those ideas that interests me right now:

not Clean Architecture as doctrine, not Hexagonal as diagram theater, not DDD as big-language ceremony,

but a pragmatic use of architectural boundaries to reduce fragility in a system that is becoming real.

Request context was the real aha moment

The concept that made the whole thing click for me was request context.

At first it looked like a technical detail. Just another object passing through the system.

Then I understood what it was really carrying:

  • who is acting
  • which tenant is active
  • what permissions and roles matter in that request

That changed how I saw the backend.

Without trusted request context, tenant information leaks into the wrong places:

  • the UI starts making authority decisions
  • route handlers start reconstructing context ad hoc
  • services stop knowing whether they are acting on trusted identity or just user input

With a proper request context, every backend operation starts from something much more stable:

the system already knows who is acting and where they are acting from.

That is why request context stopped feeling like plumbing to me. It started feeling like backbone.

The layer distinction finally became real

I had already heard the standard distinction before:

UI -> API -> Service -> Repository -> DB

But knowing the diagram is not the same as understanding why it matters.

It started mattering when I could see the failure modes more clearly.

If the UI decides tenant scope, the trust boundary is weak.

If the API layer contains business policy, rules drift and duplicate.

If services do not own the real decisions, the system becomes hard to reason about.

If the repository layer starts carrying domain meaning, persistence and policy get entangled.

Those are not just cleanliness concerns. They are the kinds of small architectural leaks that eventually turn into security, consistency, and maintainability problems.

"It works" is not a quality metric

This was probably the hardest lesson to accept.

There is a phase in building where a lot of things appear fine:

  • the happy path works
  • the UI looks coherent
  • the flow completes
  • the database writes succeed

And that creates a false sense of confidence.

But the hidden questions are more important:

  • can tenant scope be forged somewhere?
  • can authorization be bypassed in one path?
  • are there operations that should be atomic but are not?
  • are there dangerous dev shortcuts that can leak into real usage?

A system can look stable while still being architecturally weak.

That is why "it works" is such a dangerous metric. It only tells you the surface behaved once. It tells you almost nothing about whether the boundaries are sound.

Conventions are not enough for dangerous things

One of the most important changes in my thinking was this:

if something is dangerous, it should be hard or impossible, not merely discouraged.

Early on, it is tempting to rely on soft rules:

  • we just will not pass tenant from here
  • we know this module should not create users
  • this path is only for development

But conventions degrade.

People forget. Code evolves. The project grows.

And once the system depends on everyone remembering an unwritten rule, the architecture is already weaker than it should be.

This is why guardrails matter.

Not because patterns are elegant. Because dangerous behavior should not depend on memory.

Transactions changed meaning too

I also started seeing transaction boundaries differently.

At first they looked like a technical implementation detail.

But some actions in a real SaaS are not single writes. They are a bundle of consequences:

  • create an entity
  • provision identity
  • assign a role
  • write audit information

If those things belong to one business action, they should succeed together or fail together.

That is what made Unit of Work feel less like a pattern name and more like a truth guarantee.

The point is not to "apply UoW" because architecture books say so. The point is to avoid a system that can half-complete important actions and quietly leave behind inconsistent state.

What this changed in practice

The practical shift was not that I suddenly became an architect in the abstract sense.

It was that I started evaluating design choices differently.

I now trust a system more when:

  • authority is established once and carried explicitly
  • responsibility placement is visible
  • dangerous behavior is constrained by design
  • business-critical actions are coherent under failure
  • the architecture makes misuse harder, not just misuse less likely

That is a much stricter way of looking at software than "the feature works."

But for a real SaaS, it is the more honest one.

Final thought

At some point, building software stops being mainly about shipping screens and flows.

It becomes about deciding which guarantees are real, which boundaries are trusted, and which responsibilities the system can safely carry without depending on wishful thinking.

That shift does not make features less important. It just puts them in the right place.

Features are what the user sees. Guarantees are what make the system deserve to exist behind them.

Continue exploring

Follow the same line of thought through themes, tags, or a broader local search across the archive.

Keep following the thread.