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.
@Bean
MongoTemplate tenantTemplate(MultiTenantProperties props) {
String uri = props.uriFor(TenantContext.current());
return new MongoTemplate(MongoClients.create(uri), props.databaseFor(...));
}@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.
// 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.
# The application reads tenant connection strings from its own keys
bonsai.neutralise-properties=tenants.alpha.uri,tenants.beta.uriEach 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-propertiesset: 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
mongodlistening 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-propertiesvalues 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.