Factory Patterns for Multi-Tenancy
Multi-tenant apps get messy fast: per-tenant configs, data isolation, different providers, feature flags… Factories give us one clean place to create the right thing for this tenant, right now—without if/else spaghetti across the codebase.
This lab explores why factories are a natural fit for multi-tenancy, what patterns we can use, and where the traps lie.
Why factories here?
Factories aren’t just syntactic sugar. In a multi-tenant world, they give us:
- Isolation by construction. You can't accidentally use the wrong client if it never gets created for the wrong tenant.
- Swapability. Stripe today, Adyen tomorrow? S3 vs. GCS? Factories hide that choice.
- Performance knobs. Centralized caching & pooling per tenant.
- Testability. Swap implementations (mocks, in-memory) without changing call sites.
- Security. One path to inject secrets and scopes; easier to audit.
Core pattern: Request-scoped Tenant Context
Think of TenantContext as the seed for every factory. It’s the only place that knows tenant config, secret references, feature flags, and branding.
A TenantContext typically includes:
- Tenant ID: the unique identifier for this tenant.
- Environment: such as
test
orlive
. - Region: optional location to scope resources.
- Features: a map of feature flags (
newBilling: true
,aiInsights: false
). - Secrets: connection strings, account IDs, bucket names.
- Branding: tenant-specific theme or logo.
From here, factories consume the TenantContext and return scoped clients (databases, storage, payments) that know nothing beyond their own tenant.
Patterns to Use
1. Simple Factory per Service
Each service (DB, Stripe, S3) gets a function that takes the tenant context and returns a scoped client.
Pros: dead simple.
Cons: duplication if you have many services.
2. Abstract Factory
Wrap a family of services into one factory (createTenantServices
). Centralized, easier to reason about.
Pros: keeps construction logic in one place.
Cons: can become a “god factory” if abused.
3. Factory + Registry
Factories write to a per-tenant registry (cache or DI container). The next time you ask for the same service/tenant pair, you get the cached instance.
Pros: great for connection pooling and performance.
Cons: requires eviction/TTL logic or you risk memory bloat.
4. Factory with Feature Flags
Extend factories to check tenant feature flags. Example:
- If
newBilling = true
, return the new Billing client. - Else, fallback to legacy.
Pros: simplifies feature rollout.
Cons: feature flags can leak into call sites if factories don’t own the decision.
Anti-Patterns
- Inline conditionals everywhere:
Sprinklingif tenantId = A then... else...
across code makes it impossible to audit, test, or change. - Hard-coded secrets:
Injecting secrets directly instead of passing through factories means inconsistent handling and security gaps. - Mixing tenant logic into business services:
Your domain services should assume they already have the right tenant-scoped client. They should never decide which one to create.
Closing Thought
Factories aren’t glamorous, but they are discipline in code form. In a multi-tenant system, that discipline is the difference between a codebase you can extend and one you constantly fear touching.
The payoff is simple: one clean place to decide “for this tenant, right now.”