Skip to content

Build1 publisher2 min readPublished

A test suite with one tenant passes the export query that returns both customers' invoices

The BootSaaS walkthrough puts each customer's business tables in their own PostgreSQL schema, after pricing database-per-tenant at 1,000 HikariCP pools for 1,000 tenants under Spring Boot's default data source setup.

The Engineer · Build desk

Illustration accompanying A test suite with one tenant passes the export query that returns both customers' invoices

What happened

  • The walkthrough starts from a customer opening a dashboard and seeing another company's invoices, with nobody breaking in and the user correctly logged in.
  • An export endpoint running SELECT id, amount FROM invoices returns both Acme's and Globex's rows when application-level filtering is the only isolation mechanism, and the SQL is valid.
  • The walkthrough notes that such an endpoint may still pass a test suite containing only one tenant.
  • For BootSaaS the author chose schema-per-tenant on Spring Boot, PostgreSQL and Liquibase, wanting separate business tables per customer, a shared connection budget, and deployments a small team can operate.
  • Database-per-tenant is rejected on operational cost: with one HikariDataSource per database, 1,000 tenant databases means managing 1,000 separate connection pools if all are kept active.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Keeping one shared invoices table makes central enforcement mandatory infrastructure: the ORM layer or the database has to scope every endpoint anyone writes next year, because a convention will not.
  • exposure Row-level security only binds the role you connect as, so a deployment whose application runs as the owner of its tables gets policies that never apply to it.
  • cost Creating pools on demand moves creation, eviction and concurrent-access handling into code your team maintains, and that becomes the standing price of adding a workspace.
  • decision One concrete customer requirement reopens the whole choice: the author would take on separate databases to satisfy it, while declining to put that machinery on the default path.

The unfiltered SELECT is the version that is easy to picture. The same omission turns up in a lookup by ID, an update and a delete [6]. A globally unique invoice ID keeps two customers' records from colliding, and it does not establish that the current user may touch the row it names [7]. "I wouldn't build tenant isolation around everyone remembering a filter," the author wrote [8].

Two questions are being kept apart here. Tenant isolation asks which organization's data a request can reach; permissions ask whether a member of that organization may view invoices, invite teammates or change settings, and the post says you need both [24]. The model assumed throughout is one tenant per user, with several users per tenant [23].

Schema-per-tenant is one PostgreSQL database holding a separate schema per customer organization, where a schema is a named container for database objects and each tenant gets its own set of business tables inside it [20]. An unfiltered SELECT against those tables can only return rows that live in the schema the connection resolved to. Correctness moves off every predicate in every query and onto one routing decision per request. The post describes itself as an architecture walkthrough whose examples illustrate the design and are not a complete implementation [21].

For a B2B SaaS with many workspaces, broadly the same data model and a small engineering team [11], I would make the same call. A boundary you can see in the schema list is easier to audit than a predicate you have to find in every query anyone adds later.

The cost argument against separate databases lands in application code. Include the JDBC or JPA starter and Spring Boot hands you HikariCP whether you asked for it or not [14]. Configure a minimum of five idle connections in each of 1,000 active pools and those pools are being asked to hold 5,000 idle connections in total [16]. The author calls that a configuration example and not an unavoidable connection count [17].

Each pool carries its own connection objects, housekeeping and lifecycle, and holding idle connections open across hundreds of mostly inactive tenants wastes resources [25]. For any of that to bear on your deployment, your tenant count has to resemble the example's; a few dozen workspaces on one Postgres do not produce a pool problem, and the case for the schema boundary then rests entirely on where isolation is enforced. In pool terms the boundary is cheap: the same 1,000 tenants that need 1,000 pools as separate databases need one data source when they are schemas in a single database [22].

What to watch

  • Whether the follow-up publishes the schema routing code, including what a request does when it resolves to no schema.
  • A measured Liquibase migration run across hundreds of schemas would test the claim that a small team can operate this.
  • A worked row-level security comparison with a non-owner runtime role would show what the shared-table option actually costs.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories