Skip to content
Protocolzone Protocolzone

Platform to Platform · Sports & Gaming

Tenant configuration that fails safely on a multi-tenant platform

We changed a global feature setting in the admin console, the save succeeded, and behaviour on the tenant sites did not change.

This is a sports & gaming problem we approach through our Platform to Platform service line.

The problem

You run several brands on one platform, and configuration is how one codebase behaves like many products. Somewhere in the admin console is a global feature setting. An operator changes it, the console confirms the save, and nothing changes on any tenant site. Or the opposite: a setting that enables cleanly in one environment throws an error in another, complaining about missing defaults nobody knew were required.

Neither incident looks serious on its own. Together they mean the settings console can no longer be trusted, and every configuration change now goes through an engineer who inspects the database directly. That is the real cost. The console exists to keep routine changes out of the release cycle, and once its saves stop being believable, everything goes back into the release cycle.

The underlying condition is usually the same. Configuration grew key by key, each one added by whichever team needed it and read by whichever service wanted it, with no shared statement of which keys exist, what they default to, or what order overrides resolve in. Per-tenant configuration is a production data system wearing a settings UI, and it fails the way unmanaged data systems fail: quietly, and at read time.

Why it’s hard

A key with no default is an outage waiting for its first read. Configuration is written once and read constantly, by services that may have been deployed before the key existed, for tenants onboarded before it existed. Every read path either has an authored default to fall back on or it improvises — a null, an exception, or a hard-coded fallback that quietly disagrees with another service’s hard-coded fallback for the same key.

Silent non-effect is the worst failure mode a settings system has. A save that errors produces a ticket the same day. A save that succeeds without changing behaviour produces a support escalation weeks later, opened by someone who has no reason to connect it to a configuration change, and by then the operator has re-saved the value several times and stopped believing the console. The gap opens wherever the write path and the read path disagree about where a value lives: the write lands somewhere the readers never consult, and every layer between the button and the database reports success.

Resolution order is architecture, and it is usually undocumented. With three layers in play (authored default, global value, tenant override), the precedence between them decides what every tenant actually experiences. When each service carries that order in its own code, with no single written statement enforced by one resolution path, two services will eventually give the same tenant two different answers.

Update and upsert are different promises. A write path that assumes a document already exists fails document-not-found for any tenant that predates the key, which is most tenants for most keys. A write path that always creates can mask genuine identity errors. Which behaviour is correct depends on the key, so it has to be decided per key class rather than inherited from whichever code path happened to be written first.

Failure direction is a compliance decision before it is an engineering one. A responsible gambling limit that cannot be loaded must resolve to the most restrictive state, because the alternative is a customer with no limit during exactly the kind of fault window when nobody is watching. A display preference can fall back to off and nobody is harmed. Treating those two keys the same way, either both failing open or both failing closed, is wrong in one direction or the other.

How we approach it

The approach below is drawn from production. Each requirement corresponds to a defect we found and fixed on our own multi-tenant wagering platform: a global feature save that succeeded without changing behaviour, a setting that could not be enabled because keys existed without authored defaults, a global tenant configuration that had grown to hold only currency and security rules while everything else lived elsewhere, and a responsible-gambling configuration update that failed document-not-found because its write path assumed the document was already there.

A configuration registry. Every key is registered with a name, a type, a key class and an authored default. Registration is the mechanism that turns “which keys have no default” from something you discover in production into something you can list. On our platform, reconciling the global feature configuration into a registry with authored defaults is what ended the class of save-without-effect incidents.

One resolution path. Authored default, then global, then tenant override, resolved by a single shared component that every service consumes. The order is written down. Services receive resolved values; they do not re-implement precedence.

Saves that take effect observably or fail loudly. A write to an unregistered key is rejected. A write to a registered key is confirmed by resolving the value the way readers will. If the value cannot take effect, the operator finds out at the moment of the save, with a reason, long before a customer does.

Upsert semantics decided per key class. For tenant-scoped settings where absence means “never configured”, the write creates. For records whose absence indicates a fault, the write fails and says so. The decision is recorded against the key class so the next write path does not re-litigate it.

Fail closed where the safe state is restrictive. Compliance-class configuration — responsible gambling limits foremost — resolves to the most restrictive value on any fault. Fail-open is reserved for keys where the permissive state is demonstrably harmless, and that classification is made deliberately, key by key.

What we would not do. We would not bolt validation onto the admin UI and call it done. The console is one writer among several (APIs, migrations and onboarding tooling write configuration too), so enforcement that lives only in the UI is decoration. The registry sits at the data layer or it sits nowhere.

What it takes

An inventory pass. Every configuration read in the codebase, catalogued with its key, its consumer and its current fallback behaviour. This is where the keys with no authored default surface, and there are always more than anyone expects.

Authoring the defaults. Each default is a statement about how the platform behaves for a tenant that has expressed no preference. Some of those statements belong to product, and for compliance-class keys they belong to compliance. Budget for those conversations: the registry code is quick, and the conversations are the long pole.

Per-class decisions on write semantics and failure direction. Upsert or strict update, fail open or fail closed, decided and recorded per key class before migration, because settlement of those questions mid-incident is how the wrong answer gets chosen.

A backfill. Tenants onboarded before a key existed need the authored default materialised or confirmed, and someone has to decide whether each tenant’s historical implicit behaviour becomes its recorded value or gets corrected.

A gate for new keys. Registration becomes the path by which a key comes to exist, so the inventory does not decay back into folklore.

Where this has been done

On our own platform. We build and operate a multi-tenant tote and fixed-odds betting platform, and every defect described on this page is one we found there and fixed: the silent save, the missing authored defaults, the global configuration that held only currency and security rules before the registry work, and the responsible-gambling update that assumed its document already existed. The registry, the resolution order and the per-class failure rules are what that platform runs now.

The consolidation economics of running many brands on that platform are covered in the multi-tenant wagering platform case study linked from this page.

Position on this page

Evidence

Delivered work

Industry

Sports & Gaming

Written for

CTO, Head of Engineering, Operations

Outcome

A configuration registry where every key has an authored default and a written resolution order, so a save either changes behaviour observably or fails loudly at write time.

Questions we get asked

Straight answers.

What does it mean for tenant configuration to fail safely?
Two things. At write time, a save either changes behaviour observably or is rejected with a reason — it never succeeds without effect. At read time, a value that cannot be resolved falls back in a direction chosen per key class: a responsible gambling limit that fails to load must resolve to the most restrictive state, never to no limit, while a cosmetic display toggle can safely fall back to off.
Why does every configuration key need an authored default?
Because keys are written once and read constantly, often by services deployed before the key existed and for tenants onboarded before it existed. A key with no authored default forces every read path to improvise — a null, an exception, or a hard-coded fallback that disagrees with the next service's hard-coded fallback. We have had a global setting refuse to enable in production because keys existed without authored defaults; the registry is what makes that a fact you can audit rather than an error you discover.
What is the right resolution order for per-tenant configuration?
Authored default, then global value, then tenant override — the most specific value wins, and the authored default guarantees resolution always terminates. The order itself matters less than it being written down and enforced by one shared resolution path. When each service implements its own precedence, two services will eventually answer the same question differently for the same tenant.
How do you stop a configuration save from succeeding silently?
A save succeeds silently when the write path and the read path disagree about where a value lives: the write lands in a document the readers never consult, and every layer reports success. The registry closes that gap — a write to an unregistered key is rejected, and the write path confirms the value as the readers will resolve it, not merely as it was stored.

Read next

Related work.

Case studies

Is this your problem?

Bring the constraint that makes your version harder than this one. That is the part worth an hour.