Multi-tenancy: shared engine, shared tables, or neither
Motto — Tenancy is an isolation-vs-cost dial with three stops — a tag column, a schema, or an engine — and regulation, not architecture taste, usually picks the stop.
Part of Phase 10 — Architecture & product decisions. Concept lesson — no code required.
The Problem
The platform works. Now the second brand, the second country, or the second bank partner wants onboarding onto it. “Just add a tenant column” and “give everyone their own stack” are both real answers, and they differ in cost by an order of magnitude.
The wrong pick surfaces late — as an auditor’s finding (“partner A’s ops can query partner B’s loans”) or a finance finding (“we run forty engines at 2% utilisation”). Flowable supports three stops on the dial. The work is knowing which failure you’re buying.
The Concept
<style> .dgm-mt{background:var(–color-surface);border:1px solid var(–color-border);border-top:3px solid var(–color-border-danger-outline); border-radius:16px;padding:28px 26px 22px;box-shadow:0 1px 3px rgb(var(–shadow-color) / 0.04),0 8px 24px rgb(var(–shadow-color) / 0.04); margin:26px 0;max-width:100%;font-family:inherit;text-align:left;position:relative} .dgm-mt-chip{position:absolute;top:-11px;left:24px;background:var(–color-background-danger-solid);color:var(–white); font-size:10.5px;font-weight:700;letter-spacing:1px;text-transform:uppercase; border-radius:20px;padding:3px 12px;box-shadow:0 2px 6px color-mix(in oklab, var(–color-background-danger-solid) 28%, transparent)} .dgm-mt h4{margin:0 0 4px;font-size:11px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;color:var(–color-text-secondary)} .dgm-mt-sub{font-size:12px;color:var(–color-text-secondary);margin:0 0 16px} .dgm-mt-chain{display:flex;flex-direction:column;gap:2px} .dgm-mt-node{background:var(–color-background-danger-surface);border:1.5px solid var(–color-border-danger-surface);border-radius:10px;padding:9px 14px; font-weight:700;font-size:11px;color:var(–color-text-danger)} .dgm-mt-node.alt{background:var(–color-surface-tertiary);border-color:var(–color-border);color:var(–color-text)} .dgm-mt-arr{display:flex;justify-content:center;color:var(–color-text-tertiary);padding:1px 0;font-size:9.5px;text-align:center} @media (max-width:640px){.dgm-mt{padding:20px 16px 18px}} </style> <div class=“dgm-mt”> <span class=“dgm-mt-chip”>Three tenancy models</span> <h4>Row, schema, or stack — isolation goes up as shared infrastructure goes down</h4> <p class=“dgm-mt-sub”>Each step costs more to run and buys more isolation between tenants.</p> <div class=“dgm-mt-chain”> <div class=“dgm-mt-node alt”>Row tenancy — one engine, one schema, TENANT_ID_ on every row</div> <div class=“dgm-mt-arr”>↓</div> <div class=“dgm-mt-node accent”>Schema tenancy — one engine process, datasource per tenant</div> <div class=“dgm-mt-arr”>↓</div> <div class=“dgm-mt-node alt”>Stack tenancy — engine per tenant</div> </div> </div>
| Row (tenant ID) | Schema per tenant | Stack per tenant | |
|---|---|---|---|
| Isolation | logical only — every query must carry the tenant filter | hard at the data layer | hard everywhere |
| Blast radius | shared: one bad deploy/tenant storm hits all | shared compute, isolated data | none shared |
| Cost & ops | one of everything (cheapest) | one engine, N databases | N × Phase 9 |
| Per-tenant versions/models | shared definitions, or per-tenant deployments by tag | naturally per-tenant | trivially per-tenant |
| Fits | many small tenants, same product, no data-residency walls | regulated partners, residency, “your data in your database” contracts | few, large, paranoid tenants; different countries’ stacks |
Here are the Flowable mechanics for the first stop, because it’s the one with
sharp edges. Deployments, definitions, instances, tasks, and jobs all carry a
tenant ID. You set it at deployment (.tenantId("partnerA")) and at start
time, and you can filter on it in every query
(taskCandidateGroupIn(...).taskTenantId("partnerA")). The same key can be
deployed per tenant with different versions, giving you partner-specific process
variants without forking the platform.
The rules that keep each stop safe:
- Row tenancy stands or falls at the API layer. The engine stores tenant IDs; it doesn’t enforce them. Your perimeter (Phase 3.03’s claims mapping: tenant comes from the token, never the request body) must inject the tenant filter into every query and command. One missed filter is a cross-tenant leak, so make it structurally impossible with a wrapper client, not a code-review rule.
- Shared vs per-tenant definitions is a product decision. One shared model means one upgrade, and tenants move together (Phase 8 cohorts get a tenant dimension). Per-tenant deployments allow variants, but cost N migration projects. Pick per process, not platform-wide.
- Ops signals need the tenant dimension early. Run Phase 9’s probe per tenant, so one tenant’s dead-letter storm or pool depth stays attributable. Otherwise the noisy tenant hides inside platform averages until they’re everyone’s problem.
- Residency trumps elegance. A clause like “partner data stays in partner’s database, in partner’s country” ends the row-tenancy conversation regardless of cost. That’s the schema or stack stop, by contract.
Ship It
This lesson ships
outputs/tenancy-decision-guide.md — the
dial, the enforcement checklist for row tenancy, and the residency triggers.
Check Yourself
Q1. Row tenancy’s isolation is enforced by…
- A) the engine, automatically
- B) your API layer injecting the tenant (from token claims) into every query and command — the engine stores the tag, you enforce it
- C) the database
- D) the modeler
<details><summary>Answer</summary>B — the perimeter owns tenancy exactly as it owns identity (Phase 3.03). Structural enforcement beats vigilance.</details>
Q2. A partner contract requires “our workflow data in our own database, in our country”. The dial stop is…
- A) row — add an index
- B) schema-per-tenant at minimum, stack if the runtime must be resident too — residency clauses end the shared-tables option
- C) encryption
- D) negotiable
<details><summary>Answer</summary>B — rule 4. Contracts and regulators pick this stop; architecture just prices it.</details>
Q3. Deploying loanOrigination separately per tenant buys… at the cost of…
- A) nothing / nothing
- B) per-partner process variants / N versioning-and-migration projects instead of one (Phase 8, multiplied)
- C) speed / memory
- D) isolation / licenses
<details><summary>Answer</summary>B — variant freedom is real product value; its price is every Phase 8 discipline times N. Choose per process.</details>
Challenge. Take the capstone platform and onboard two hypothetical tenants: a sister brand (same product, same country) and a bank partner (residency clause, custom review step). Place each on the dial. List the enforcement mechanics for the first, and the migration-cost line for the second. You’ll usually land on two different stops — that’s the mixed-tenancy reality most platforms run.
Related
- Next: Where the process ends and the domain begins
- The perimeter that enforces it: Phase 3, lesson 03
