0
# Multi-Tenancy Guide
1
2
FoundationDB provides built-in multi-tenancy support, allowing multiple applications or users to share the same database cluster while maintaining complete data isolation. Each tenant gets its own isolated keyspace that is invisible to other tenants.
3
4
## Core Concepts
5
6
- **Tenant**: An isolated keyspace within a FoundationDB cluster
7
- **Tenant Name**: Unique identifier for a tenant (byte array)
8
- **Tenant Transaction**: Transaction scoped to a specific tenant's keyspace
9
- **Data Isolation**: Complete separation of data between tenants
10
- **Shared Infrastructure**: All tenants share the same cluster resources
11
12
## Capabilities
13
14
### Administrative Tenant Management
15
16
Functions for creating, deleting, and listing tenants in the cluster. These are administrative operations that manage tenant metadata.
17
18
```python { .api }
19
# Python API - Administrative functions (fdb.tenant_management module)
20
def create_tenant(db_or_tr, tenant_name: bytes) -> None:
21
"""
22
Create a new tenant in the cluster.
23
24
Args:
25
db_or_tr: Database or Transaction handle
26
tenant_name: Unique name for the tenant
27
28
Raises:
29
FDBError(2132): tenant_already_exists if tenant exists
30
"""
31
32
def delete_tenant(db_or_tr, tenant_name: bytes) -> None:
33
"""
34
Delete a tenant from the cluster.
35
36
Args:
37
db_or_tr: Database or Transaction handle
38
tenant_name: Name of tenant to delete
39
40
Raises:
41
FDBError(2131): tenant_not_found if tenant doesn't exist
42
"""
43
44
def list_tenants(db_or_tr, begin: bytes, end: bytes, limit: int) -> FDBTenantList:
45
"""
46
List tenants in the cluster within the specified range.
47
48
Args:
49
db_or_tr: Database or Transaction handle
50
begin: Start of tenant name range (inclusive)
51
end: End of tenant name range (exclusive)
52
limit: Maximum number of tenants to return
53
54
Returns:
55
Iterable of KeyValue objects with tenant names and metadata
56
"""
57
58
class FDBTenantList:
59
"""Iterator for tenant listing results."""
60
def to_list(self) -> List[KeyValue]: ...
61
def __iter__(self) -> Iterator[KeyValue]: ...
62
```
63
64
```c { .api }
65
// C API - Administrative tenant operations
66
DLLEXPORT WARN_UNUSED_RESULT fdb_error_t fdb_database_open_tenant(FDBDatabase* d,
67
uint8_t const* tenant_name,
68
int tenant_name_length,
69
FDBTenant** out_tenant);
70
71
DLLEXPORT WARN_UNUSED_RESULT fdb_error_t fdb_tenant_create_transaction(FDBTenant* tenant,
72
FDBTransaction** out_transaction);
73
74
DLLEXPORT WARN_UNUSED_RESULT FDBFuture* fdb_tenant_get_id(FDBTenant* tenant);
75
76
DLLEXPORT void fdb_tenant_destroy(FDBTenant* tenant);
77
```
78
79
### Tenant Operations and Management
80
81
```python { .api }
82
# Python API
83
class Database:
84
def open_tenant(self, tenant_name: bytes) -> Tenant:
85
"""
86
Open tenant handle for multi-tenancy operations.
87
88
Args:
89
tenant_name: Unique name for the tenant
90
91
Returns:
92
Tenant handle with isolated keyspace
93
"""
94
95
class Tenant:
96
def create_transaction(self) -> Transaction:
97
"""
98
Create transaction within this tenant's keyspace.
99
All operations are automatically scoped to tenant.
100
101
Returns:
102
Transaction handle scoped to tenant
103
"""
104
```
105
106
```java { .api }
107
// Java API
108
public interface Database {
109
Tenant openTenant(byte[] tenantName);
110
}
111
112
public interface Tenant extends Transactor, ReadTransactor {
113
Transaction createTransaction();
114
byte[] getName();
115
}
116
```
117
118
```go { .api }
119
// Go API
120
type Database interface {
121
OpenTenant(tenantName []byte) (Tenant, error)
122
}
123
124
type Tenant interface {
125
CreateTransaction() (Transaction, error)
126
Transact(func(Transaction) (interface{}, error)) (interface{}, error)
127
ReadTransact(func(ReadTransaction) (interface{}, error)) (interface{}, error)
128
}
129
```
130
131
```ruby { .api }
132
# Ruby API
133
class FDB::Database
134
def open_tenant(tenant_name)
135
end
136
137
class FDB::Tenant
138
def create_transaction
139
def transact(&block)
140
def name
141
end
142
```
143
144
### Cross-Language Usage Patterns
145
146
**Python Example:**
147
148
```python
149
import fdb
150
151
fdb.api_version(740)
152
db = fdb.open()
153
154
# Open tenant
155
app1_tenant = db.open_tenant(b"app1")
156
app2_tenant = db.open_tenant(b"app2")
157
158
@fdb.transactional
159
def store_user_data(tenant, user_id, data):
160
tr = tenant.create_transaction()
161
tr.set(f"user:{user_id}".encode(), data.encode())
162
163
# Data is isolated per tenant
164
store_user_data(app1_tenant, "user123", "app1_data")
165
store_user_data(app2_tenant, "user123", "app2_data") # Different keyspace
166
```
167
168
**Java Example:**
169
170
```java
171
FDB fdb = FDB.selectAPIVersion(740);
172
Database db = fdb.open();
173
174
// Open tenants
175
Tenant app1Tenant = db.openTenant("app1".getBytes());
176
Tenant app2Tenant = db.openTenant("app2".getBytes());
177
178
// Each tenant has isolated data
179
String app1Result = app1Tenant.run(tr -> {
180
tr.set("user:123".getBytes(), "app1_data".getBytes());
181
return "stored";
182
});
183
184
String app2Result = app2Tenant.run(tr -> {
185
tr.set("user:123".getBytes(), "app2_data".getBytes());
186
return "stored";
187
});
188
```
189
190
**Go Example:**
191
192
```go
193
fdb.APIVersion(740)
194
fdb.StartNetwork()
195
defer fdb.StopNetwork()
196
197
db, _ := fdb.OpenDatabase("")
198
199
// Open tenants
200
app1Tenant, _ := db.OpenTenant([]byte("app1"))
201
app2Tenant, _ := db.OpenTenant([]byte("app2"))
202
203
// Isolated operations
204
app1Tenant.Transact(func(tr fdb.Transaction) (interface{}, error) {
205
tr.Set(fdb.Key("user:123"), []byte("app1_data"))
206
return nil, nil
207
})
208
209
app2Tenant.Transact(func(tr fdb.Transaction) (interface{}, error) {
210
tr.Set(fdb.Key("user:123"), []byte("app2_data"))
211
return nil, nil
212
})
213
```
214
215
**Ruby Example:**
216
217
```ruby
218
FDB.api_version(740)
219
db = FDB.open
220
221
# Open tenants
222
app1_tenant = db.open_tenant("app1")
223
app2_tenant = db.open_tenant("app2")
224
225
# Isolated transactions
226
app1_tenant.transact do |tr|
227
tr["user:123"] = "app1_data"
228
end
229
230
app2_tenant.transact do |tr|
231
tr["user:123"] = "app2_data" # Completely separate from app1
232
end
233
```
234
235
## Multi-Tenant Application Patterns
236
237
### Tenant-per-Customer Pattern
238
239
```python
240
class MultiTenantService:
241
def __init__(self, database):
242
self.db = database
243
self.tenant_cache = {}
244
245
def get_tenant(self, customer_id):
246
if customer_id not in self.tenant_cache:
247
tenant_name = f"customer_{customer_id}".encode()
248
self.tenant_cache[customer_id] = self.db.open_tenant(tenant_name)
249
return self.tenant_cache[customer_id]
250
251
@fdb.transactional
252
def store_customer_data(self, customer_id, key, value):
253
tenant = self.get_tenant(customer_id)
254
tr = tenant.create_transaction()
255
tr.set(key.encode(), value.encode())
256
257
@fdb.transactional
258
def get_customer_data(self, customer_id, key):
259
tenant = self.get_tenant(customer_id)
260
tr = tenant.create_transaction()
261
return tr.get(key.encode()).wait()
262
```
263
264
### Tenant-per-Application Pattern
265
266
```java
267
public class ApplicationTenantManager {
268
private final Database db;
269
private final Map<String, Tenant> tenants = new ConcurrentHashMap<>();
270
271
public ApplicationTenantManager(Database db) {
272
this.db = db;
273
}
274
275
public Tenant getTenant(String appName) {
276
return tenants.computeIfAbsent(appName, name ->
277
db.openTenant(name.getBytes()));
278
}
279
280
public void storeAppData(String appName, String key, String value) {
281
Tenant tenant = getTenant(appName);
282
tenant.run(tr -> {
283
tr.set(key.getBytes(), value.getBytes());
284
return null;
285
});
286
}
287
288
public String getAppData(String appName, String key) {
289
Tenant tenant = getTenant(appName);
290
return tenant.read(tr -> {
291
byte[] value = tr.get(key.getBytes()).join();
292
return value != null ? new String(value) : null;
293
});
294
}
295
}
296
```
297
298
### Environment-based Tenancy
299
300
```go
301
type EnvironmentManager struct {
302
db fdb.Database
303
tenants map[string]fdb.Tenant
304
mutex sync.RWMutex
305
}
306
307
func NewEnvironmentManager(db fdb.Database) *EnvironmentManager {
308
return &EnvironmentManager{
309
db: db,
310
tenants: make(map[string]fdb.Tenant),
311
}
312
}
313
314
func (em *EnvironmentManager) GetTenant(environment string) (fdb.Tenant, error) {
315
em.mutex.RLock()
316
tenant, exists := em.tenants[environment]
317
em.mutex.RUnlock()
318
319
if exists {
320
return tenant, nil
321
}
322
323
em.mutex.Lock()
324
defer em.mutex.Unlock()
325
326
// Double-check after acquiring write lock
327
if tenant, exists := em.tenants[environment]; exists {
328
return tenant, nil
329
}
330
331
tenant, err := em.db.OpenTenant([]byte(environment))
332
if err != nil {
333
return nil, err
334
}
335
336
em.tenants[environment] = tenant
337
return tenant, nil
338
}
339
340
func (em *EnvironmentManager) StoreData(environment, key, value string) error {
341
tenant, err := em.GetTenant(environment)
342
if err != nil {
343
return err
344
}
345
346
_, err = tenant.Transact(func(tr fdb.Transaction) (interface{}, error) {
347
tr.Set(fdb.Key(key), []byte(value))
348
return nil, nil
349
})
350
351
return err
352
}
353
```
354
355
### Dynamic Tenant Management
356
357
```ruby
358
class DynamicTenantManager
359
def initialize(database)
360
@db = database
361
@tenant_registry = {}
362
@mutex = Mutex.new
363
end
364
365
def register_tenant(tenant_id, metadata = {})
366
@mutex.synchronize do
367
tenant_name = "tenant_#{tenant_id}"
368
369
@tenant_registry[tenant_id] = {
370
name: tenant_name,
371
tenant: @db.open_tenant(tenant_name),
372
metadata: metadata,
373
created_at: Time.now
374
}
375
end
376
end
377
378
def get_tenant(tenant_id)
379
@mutex.synchronize do
380
entry = @tenant_registry[tenant_id]
381
entry&.fetch(:tenant)
382
end
383
end
384
385
def execute_for_tenant(tenant_id, &block)
386
tenant = get_tenant(tenant_id)
387
raise "Tenant not found: #{tenant_id}" unless tenant
388
389
tenant.transact(&block)
390
end
391
392
def list_tenants
393
@mutex.synchronize do
394
@tenant_registry.map do |id, entry|
395
{
396
id: id,
397
name: entry[:name],
398
metadata: entry[:metadata],
399
created_at: entry[:created_at]
400
}
401
end
402
end
403
end
404
405
def tenant_statistics(tenant_id)
406
execute_for_tenant(tenant_id) do |tr|
407
# Get basic statistics about tenant data
408
count = tr.get_range('', "\xFF").count
409
{
410
tenant_id: tenant_id,
411
key_count: count,
412
sampled_at: Time.now
413
}
414
end
415
end
416
end
417
```
418
419
## Best Practices
420
421
### Tenant Naming Conventions
422
423
- Use consistent, predictable naming patterns
424
- Include version or environment information when needed
425
- Avoid special characters that might cause issues
426
- Keep names reasonably short but descriptive
427
428
```python
429
# Good tenant naming patterns
430
tenant_name = f"app_{app_name}_v{version}".encode()
431
tenant_name = f"customer_{customer_id}".encode()
432
tenant_name = f"env_{environment}_{service}".encode()
433
434
# Avoid
435
tenant_name = user_input.encode() # Unpredictable
436
tenant_name = f"a_very_long_tenant_name_that_contains_lots_of_information".encode()
437
```
438
439
### Tenant Lifecycle Management
440
441
```python
442
class TenantLifecycleManager:
443
def __init__(self, database):
444
self.db = database
445
self.active_tenants = set()
446
447
def provision_tenant(self, tenant_id, config=None):
448
tenant_name = f"tenant_{tenant_id}".encode()
449
tenant = self.db.open_tenant(tenant_name)
450
451
# Initialize tenant with default data
452
self._initialize_tenant_schema(tenant, config)
453
454
self.active_tenants.add(tenant_id)
455
return tenant
456
457
def _initialize_tenant_schema(self, tenant, config):
458
@fdb.transactional
459
def setup(tr):
460
tenant_tr = tenant.create_transaction()
461
462
# Set up initial configuration
463
tenant_tr.set(b'_config', json.dumps(config or {}).encode())
464
tenant_tr.set(b'_created_at', str(time.time()).encode())
465
tenant_tr.set(b'_version', b'1.0')
466
467
setup(self.db)
468
469
def deprovision_tenant(self, tenant_id):
470
# Note: Actual tenant deletion requires administrative operations
471
# This just removes from active set
472
self.active_tenants.discard(tenant_id)
473
474
def migrate_tenant_data(self, from_tenant_id, to_tenant_id):
475
from_tenant = self.db.open_tenant(f"tenant_{from_tenant_id}".encode())
476
to_tenant = self.db.open_tenant(f"tenant_{to_tenant_id}".encode())
477
478
@fdb.transactional
479
def migrate(tr):
480
from_tr = from_tenant.create_transaction()
481
to_tr = to_tenant.create_transaction()
482
483
# Copy all data from source to destination
484
for kv in from_tr.get_range(b'', b'\xFF'):
485
to_tr.set(kv.key, kv.value)
486
487
migrate(self.db)
488
```
489
490
### Error Handling in Multi-Tenant Applications
491
492
```java
493
public class TenantErrorHandler {
494
public <T> T executeWithTenant(String tenantId, Function<Tenant, T> operation) {
495
try {
496
Tenant tenant = getTenant(tenantId);
497
return operation.apply(tenant);
498
} catch (FDBException e) {
499
if (isTenantNotFound(e)) {
500
throw new TenantNotFoundException("Tenant not found: " + tenantId);
501
} else if (isTenantInaccessible(e)) {
502
throw new TenantInaccessibleException("Tenant temporarily unavailable: " + tenantId);
503
} else {
504
throw new TenantOperationException("Operation failed for tenant: " + tenantId, e);
505
}
506
}
507
}
508
509
private boolean isTenantNotFound(FDBException e) {
510
// Check specific error codes for tenant not found
511
return e.getCode() == 2131; // tenant_not_found
512
}
513
514
private boolean isTenantInaccessible(FDBException e) {
515
// Check for temporary tenant issues
516
return e.getCode() == 2132; // tenant_not_accessible
517
}
518
}
519
```
520
521
## Security Considerations
522
523
### Tenant Isolation Verification
524
525
```python
526
def verify_tenant_isolation():
527
"""Verify that tenants cannot access each other's data."""
528
529
tenant_a = db.open_tenant(b"tenant_a")
530
tenant_b = db.open_tenant(b"tenant_b")
531
532
# Store data in tenant A
533
@fdb.transactional
534
def store_in_a(tr):
535
tenant_tr = tenant_a.create_transaction()
536
tenant_tr.set(b'secret_key', b'tenant_a_secret')
537
538
store_in_a(db)
539
540
# Try to read from tenant B (should not see tenant A's data)
541
@fdb.transactional
542
def read_from_b(tr):
543
tenant_tr = tenant_b.create_transaction()
544
value = tenant_tr.get(b'secret_key').wait()
545
assert value is None, "Tenant isolation violated!"
546
547
# Also verify tenant B can't see tenant A's keyspace
548
count = 0
549
for kv in tenant_tr.get_range(b'', b'\xFF'):
550
count += 1
551
552
assert count == 0, "Tenant B should not see any keys from tenant A"
553
554
read_from_b(db)
555
```
556
557
### Tenant Access Control
558
559
```ruby
560
class TenantAccessController
561
def initialize(database)
562
@db = database
563
@access_policies = {}
564
end
565
566
def define_policy(tenant_id, user_id, permissions)
567
@access_policies["#{tenant_id}:#{user_id}"] = permissions
568
end
569
570
def check_access(tenant_id, user_id, operation)
571
policy_key = "#{tenant_id}:#{user_id}"
572
permissions = @access_policies[policy_key] || []
573
574
unless permissions.include?(operation)
575
raise "Access denied: #{user_id} cannot #{operation} on tenant #{tenant_id}"
576
end
577
end
578
579
def execute_with_access_check(tenant_id, user_id, operation, &block)
580
check_access(tenant_id, user_id, operation)
581
582
tenant = @db.open_tenant("tenant_#{tenant_id}")
583
tenant.transact(&block)
584
end
585
end
586
```
587
588
Multi-tenancy in FoundationDB provides robust isolation while maintaining high performance and operational simplicity. Each tenant operates as if it has its own dedicated database while sharing the underlying infrastructure efficiently.