Archiboard

Set K · Craft & Judgment

Saying no to the feature that breaks the tenancy model

A report across sister companies, a user shared between tenants. Each sounds small, and each turns the tenant boundary into a suggestion. Say no with a drawing, and bring the alternative on the same sheet.

The request arrived in January 2024 as a one-line ticket from the account manager: "Customer X wants one report of activity across all their companies." Customer X was a holding with seven operating companies, each one a tenant of a product I was responsible for, each one with its own tenant row and its own users. The report was, in their eyes, a filter. Seven checkboxes instead of one. The account manager had already said it sounded easy.

Four months later a different customer asked for something that also sounded easy: one login for their accountant, who did the books for four of our tenants and was tired of four passwords. That one arrived with a screenshot of a competitor that allowed it.

Both requests were reasonable. Both were, from the customer's chair, obviously correct. And both of them, drawn out, put an arrow across the one line in the system that everything else depends on. This sheet is about how I say no to that arrow, and about what I put on the same page so that the no is not the end of the conversation.

The line that everything depends on

In a shared-row product the tenant boundary is not a wall. It is a TenantId column, a query filter, and a promise. The promise is that no request ever reads or writes rows belonging to another tenant, and that promise is enforced in exactly one place: the resolved tenant that the middleware puts in a scoped service before anything else runs. Every query filter and every authorisation check trusts that value.

A cross-tenant report needs a query that runs with seven tenants at once. The moment there is a code path that does that, the promise has an exception, and exceptions in a promise are the whole promise. Someone will reuse the "multi-tenant query" helper for something else. Someone will add an eighth tenant to the list because the customer bought another company. Someone will, in 2025, write a background job that runs the report for every holding and cache the result in a table without a TenantId column. I have seen all three; the third one was mine.

The shared user does the same thing from the identity side. A user who belongs to four tenants is a user whose token, at some point, has to say which tenant they are acting for. If the answer is "whichever one the URL says", the tenant is now resolved from a user-controlled value, and the boundary depends on the accountant never mistyping. If the answer is "all four", the token cannot be used to resolve a tenant at all.

Two requests drawn as arrows crossing the tenant boundary tenant boundary tenant A (company 1) API, tenant = A rows of A reads tenant B (company 2) API, tenant = B rows of B report reads rows of B one login which tenant is the token for? the arrow the ticket did not mention
Fig. 1. The report and the shared login, drawn as what they are. Both cross the hatched strip, and the hatched strip is the only thing that keeps company 1's rows out of company 2's screens.

Neither request is malicious and neither is unusual. The multitenant guidance in the Architecture Center calls the second one "user and tenant conflation" and lists it as an antipattern, which is a polite way of saying everyone does it once.

Saying no with a drawing

I do not say no in a meeting, or in a ticket comment. I draw the request. One sheet: the tenant boundary as a hatched strip, the tenants either side of it, and the request as a single arrow that crosses. Then I put the sheet on the table.

The drawing does the arguing. An account manager who reads "seven checkboxes" as a filter reads the arrow as what it is: a thing that reaches from one company's data into another's. The question changes from "why is this hard" to "who is allowed to draw that arrow", and that is the right question, because the answer is a legal one before it is a technical one. The holding's seven companies were separate legal entities with separate boards, one of them was being sold that summer, and what happened inside one was not the holding's to read without a paper trail we did not have.

The drawing also keeps me honest. Twice I have drawn a request expecting to say no and found the arrow did not cross the line at all. The customer who wanted "their own database" wanted a backup they could download, which is a feature, not a tenancy change.

The alternative on the same sheet

A no without an alternative is a lost customer, and often a deserved loss. So the second half of the sheet, drawn in the same sitting, shows how to give them what they asked for without the arrow.

For the report, the alternative is a level above the tenant. Call it an organisation or a holding; the name matters less than the fact that it is a first-class thing in the model with its own identifier and its own membership list. Each tenant publishes the figures it agrees to share (monthly totals, not the rows they came from) into a read model that belongs to the organisation. The publishing is a job inside the tenant, running as that tenant, writing outward. The report reads the organisation's read model and never touches a tenant table. The boundary stays where it was; the data crosses it once, in one direction, by an explicit act, and the act is auditable.

This costs more than a query with seven tenant IDs in it. It also survives the holding buying an eighth company, and the day one is sold and must stop appearing in the report. The query with seven IDs survives neither.

For the shared user, the alternative is memberships. The person is one identity; the identity has four memberships, one per tenant, each with its own role. A token is issued for one membership at a time, with the tenant identifier in it, and switching tenants means asking for a new token. The accountant gets one password and a tenant picker. The middleware still resolves one tenant per request from a claim it trusts, not from a URL. Entra External ID models this well now; in 2024 we built it ourselves in an afternoon and spent a week on the picker.

The two alternatives that keep the boundary organisation: the report reads only here read model one crossing, outward, as the tenant, audited tenant A publish job rows of A reads totals publishes tenant B publish job rows of B reads totals publishes the shared accountant one identity membership A membership B has, with a role each token, tenant = A one at a time API resolves tenant sends switching tenant means a new token
Fig. 2. The alternatives. Left: each tenant publishes agreed totals outward into a read model the organisation owns. Right: one person, several memberships, one tenant per token.

What I would trade today

I would still refuse the arrow, every time. What I have changed my mind about is the cost of the alternative. In 2024 I described the organisation read model as a large piece of work; having built it twice since, it is one table and one job, and the report on top is ordinary. The shared user took longer, mostly in the picker. Both were cheaper than the incident I would otherwise be writing about in this sheet.

Drawn from