Guides

Multi-tenant applications

Applications that construct MongoDB clients themselves, the usual shape of per-tenant database routing, need one small seam before the harness has anything to replace.

The problem

Bean overrides work by replacing beans. When an application declares a MongoClient or a MongoDatabaseFactory, the harness swaps it and everything downstream, including templates, repositories, and the transaction manager, follows.

Multi-tenant applications typically do not declare a client. They call MongoClients.create(uri) at request time, with a URI chosen from the current tenant. There is no bean to replace, so the harness has no way in, and the application dials whatever the configuration says. In a test, that is either nothing or, worse, something real.

The seam

Route client creation through a bean named mongoClientFactory, typed Function<String, MongoClient>. In production it is a method reference and behaves identically to the direct call. In a test the harness replaces it with a function that ignores the URI and hands back the embedded client, logging one line per replacement.

before: untestable
@Bean
MongoTemplate tenantTemplate(MultiTenantProperties props) {
    String uri = props.uriFor(TenantContext.current());
    return new MongoTemplate(MongoClients.create(uri), props.databaseFor(...));
}
after: seamed
@Bean
Function<String, MongoClient> mongoClientFactory() {
    return MongoClients::create;
}

@Bean
MongoTemplate tenantTemplate(MultiTenantProperties props,
                             Function<String, MongoClient> mongoClientFactory) {
    String uri = props.uriFor(TenantContext.current());
    return new MongoTemplate(mongoClientFactory.apply(uri), props.databaseFor(...));
}

What the harness will and will not replace

The post-processor matches on the bean name and on the generic signature declared by the bean definition. This is deliberately strict: replacing a function whose contract it cannot verify would be exactly the kind of quiet wrongness the library refuses elsewhere.

Java
// Replaced: the generic signature matches exactly
Function<String, MongoClient> mongoClientFactory() { ... }

// Left alone: different generics, so the harness cannot know it is safe
Function<String, Object> mongoClientFactory() { ... }
Function<String, MyClientWrapper> mongoClientFactory() { ... }

// Left alone: right type, wrong name
Function<String, MongoClient> tenantClientFactory() { ... }

When the generics cannot be resolved at all, whether because of a raw type or no bean definition to inspect, the harness falls back to matching on name plus instanceof Function, and logs the decision at debug level. Every non-replacement is logged, so a seam that silently did not take effect is one log line away from being obvious.

Bean overrides can be turned off entirely with bonsai.bean-overrides.enabled=false, which removes the post-processor and with it the seam.

neutralise-properties

The harness rewrites spring.mongodb.uri and friends so nothing can reach a real server by accident. An application with its own connection-string keys needs to name them.

Properties
# The application reads tenant connection strings from its own keys
bonsai.neutralise-properties=tenants.alpha.uri,tenants.beta.uri

Each named property is rewritten to a neutral local URI with ?serverSelectionTimeoutMS=100 appended, so a client that still dials out fails in about a tenth of a second instead of hanging for the driver's default thirty.

Support matrix

  • Seam adopted: full support: per-tenant templates, conversions, and tenant-scoped transactions including commit and rollback.
  • Seam not adopted, neutralise-properties set: the context boots and cannot dial production; the first real use fails fast.
  • Neither: the raw client dials whatever the custom property says, exactly as it did before the harness was added.

Hazards worth knowing

  • Re-binding the tenant mid-transaction commits through the wrong routing manager. This happens in production too; the harness reproduces it faithfully rather than defending against it, which means a test can catch it.
  • A real mongod listening on localhost:1 on a developer machine defeats the neutral URI. The un-seamed client connects to that instead of failing.
  • An application that closes the clients its factory produced would close the shared embedded client, since the factory hands back the same instance every time.
  • neutralise-properties values must be URI-shaped. The rewrite produces a URI; naming a property that holds something else will produce something else's idea of nonsense.

See also Configuration for the property reference, and Transactions for the transaction semantics these tenant-scoped tests rely on.