Capsule for Multi-Tenancy in Java: Comprehensive Implementation Guide

Capsule is a powerful framework for building multi-tenant Java applications that provides tenant isolation, data partitioning, and resource management. It enables running multiple tenants securely within a single application instance.


Core Concepts

What is Multi-Tenancy?

  • Single application instance serving multiple tenants (customers)
  • Each tenant's data and configuration is isolated
  • Shared infrastructure with logical separation
  • Cost-effective and scalable architecture

Key Benefits of Capsule:

  • Tenant Isolation: Secure separation between tenants
  • Resource Management: Controlled resource allocation per tenant
  • Dynamic Configuration: Tenant-specific configuration at runtime
  • Unified Codebase: Single codebase for all tenants

Architecture Overview

Application Instance
├── Tenant A Capsule
│   ├── Data Source A
│   ├── Configuration A
│   └── ClassLoader A
├── Tenant B Capsule
│   ├── Data Source B
│   ├── Configuration B
│   └── ClassLoader B
└── Shared Services
├── Tenant Registry
├── Resource Manager
└── Security Service

Dependencies and Setup

Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<hibernate.version>6.2.7.Final</hibernate.version>
<capsule.version>1.0.0</capsule.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- Multi-tenancy -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- Custom Capsule Framework -->
<dependency>
<groupId>com.example</groupId>
<artifactId>capsule-framework</artifactId>
<version>${capsule.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Application Configuration
# application.yml
spring:
application:
name: multi-tenant-app
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
app:
multi-tenancy:
enabled: true
strategy: DATABASE  # DATABASE, SCHEMA, DISCRIMINATOR
default-tenant: default
capsule:
max-tenants: 100
isolation-level: DATABASE
resource-limits:
max-memory-mb: 512
max-threads: 50
max-connections: 10
management:
endpoints:
web:
exposure:
include: health,info,metrics,tenants
endpoint:
tenants:
enabled: true
server:
port: 8080
logging:
level:
com.example.capsule: DEBUG

Core Capsule Framework

1. Tenant Context Management
public class TenantContext {
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
private static final ThreadLocal<TenantConfig> currentConfig = new ThreadLocal<>();
private static final ThreadLocal<Map<String, Object>> tenantAttributes = new ThreadLocal<>();
public static void setCurrentTenant(String tenantId) {
currentTenant.set(tenantId);
tenantAttributes.set(new HashMap<>());
}
public static String getCurrentTenant() {
return currentTenant.get();
}
public static void setTenantConfig(TenantConfig config) {
currentConfig.set(config);
}
public static TenantConfig getCurrentConfig() {
return currentConfig.get();
}
public static void setAttribute(String key, Object value) {
Map<String, Object> attributes = tenantAttributes.get();
if (attributes != null) {
attributes.put(key, value);
}
}
public static Object getAttribute(String key) {
Map<String, Object> attributes = tenantAttributes.get();
return attributes != null ? attributes.get(key) : null;
}
public static void clear() {
currentTenant.remove();
currentConfig.remove();
tenantAttributes.remove();
}
public static boolean isTenantSet() {
return currentTenant.get() != null;
}
}
2. Tenant Model
@Entity
@Table(name = "tenants")
public class Tenant {
@Id
private String id;
@Column(nullable = false, unique = true)
private String name;
@Column(nullable = false)
private String domain;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private TenantStatus status = TenantStatus.ACTIVE;
@Column(nullable = false)
private String databaseUrl;
private String databaseUsername;
private String databasePassword;
@ElementCollection
@CollectionTable(name = "tenant_configuration", joinColumns = @JoinColumn(name = "tenant_id"))
@MapKeyColumn(name = "config_key")
@Column(name = "config_value")
private Map<String, String> configuration = new HashMap<>();
@Embedded
private ResourceLimits resourceLimits;
@CreationTimestamp
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
// Constructors, getters, setters
public Tenant() {}
public Tenant(String id, String name, String domain) {
this.id = id;
this.name = name;
this.domain = domain;
}
}
@Embeddable
public class ResourceLimits {
private Integer maxUsers;
private Integer maxStorageMB;
private Integer maxApiCallsPerMinute;
private Integer maxMemoryMB;
private Integer maxDatabaseConnections;
// Constructors, getters, setters
}
public enum TenantStatus {
ACTIVE, SUSPENDED, PENDING, DELETED
}
3. Capsule Interface
public interface TenantCapsule {
String getTenantId();
String getTenantName();
TenantStatus getStatus();
// Resource Management
void initialize() throws CapsuleException;
void shutdown() throws CapsuleException;
boolean isInitialized();
// Configuration
TenantConfig getConfig();
void updateConfig(TenantConfig config) throws CapsuleException;
// Resource Access
DataSource getDataSource();
ExecutorService getExecutorService();
CacheManager getCacheManager();
// Monitoring
CapsuleMetrics getMetrics();
HealthCheckResult healthCheck();
}
public class CapsuleMetrics {
private final long memoryUsage;
private final int activeConnections;
private final int activeThreads;
private final double cpuUsage;
private final Instant lastActivity;
// Constructor, getters
}
public class HealthCheckResult {
private final boolean healthy;
private final String message;
private final Map<String, Object> details;
// Constructor, getters
}
4. Base Capsule Implementation
@Slf4j
public abstract class AbstractTenantCapsule implements TenantCapsule {
protected final String tenantId;
protected final Tenant tenant;
protected final TenantConfig config;
protected DataSource dataSource;
protected ExecutorService executorService;
protected CacheManager cacheManager;
protected volatile boolean initialized = false;
protected final Object initializationLock = new Object();
public AbstractTenantCapsule(Tenant tenant) {
this.tenantId = tenant.getId();
this.tenant = tenant;
this.config = new TenantConfig(tenant.getConfiguration());
}
@Override
public void initialize() throws CapsuleException {
synchronized (initializationLock) {
if (initialized) {
return;
}
try {
log.info("Initializing capsule for tenant: {}", tenantId);
// Initialize data source
initializeDataSource();
// Initialize thread pool
initializeExecutorService();
// Initialize cache
initializeCache();
// Perform tenant-specific initialization
performCustomInitialization();
initialized = true;
log.info("Capsule initialized successfully for tenant: {}", tenantId);
} catch (Exception e) {
log.error("Failed to initialize capsule for tenant: {}", tenantId, e);
cleanup();
throw new CapsuleException("Capsule initialization failed", e);
}
}
}
@Override
public void shutdown() throws CapsuleException {
synchronized (initializationLock) {
if (!initialized) {
return;
}
try {
log.info("Shutting down capsule for tenant: {}", tenantId);
// Shutdown executor service
if (executorService != null && !executorService.isShutdown()) {
executorService.shutdown();
try {
if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
// Close data source
if (dataSource instanceof AutoCloseable) {
try {
((AutoCloseable) dataSource).close();
} catch (Exception e) {
log.warn("Error closing data source for tenant: {}", tenantId, e);
}
}
// Perform custom cleanup
performCustomCleanup();
initialized = false;
log.info("Capsule shutdown completed for tenant: {}", tenantId);
} catch (Exception e) {
log.error("Error during capsule shutdown for tenant: {}", tenantId, e);
throw new CapsuleException("Capsule shutdown failed", e);
}
}
}
protected abstract void initializeDataSource() throws CapsuleException;
protected abstract void initializeExecutorService() throws CapsuleException;
protected abstract void initializeCache() throws CapsuleException;
protected void performCustomInitialization() throws CapsuleException {}
protected void performCustomCleanup() throws CapsuleException {}
private void cleanup() {
try {
if (executorService != null) {
executorService.shutdownNow();
}
} catch (Exception e) {
log.warn("Error during cleanup for tenant: {}", tenantId, e);
}
}
// Getters
@Override
public String getTenantId() { return tenantId; }
@Override
public String getTenantName() { return tenant.getName(); }
@Override
public TenantStatus getStatus() { return tenant.getStatus(); }
@Override
public TenantConfig getConfig() { return config; }
@Override
public DataSource getDataSource() { return dataSource; }
@Override
public ExecutorService getExecutorService() { return executorService; }
@Override
public CacheManager getCacheManager() { return cacheManager; }
@Override
public boolean isInitialized() { return initialized; }
}
5. Database-Per-Tenant Capsule
@Slf4j
public class DatabasePerTenantCapsule extends AbstractTenantCapsule {
private static final int DEFAULT_MAX_POOL_SIZE = 10;
public DatabasePerTenantCapsule(Tenant tenant) {
super(tenant);
}
@Override
protected void initializeDataSource() throws CapsuleException {
try {
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl(tenant.getDatabaseUrl());
hikariConfig.setUsername(tenant.getDatabaseUsername());
hikariConfig.setPassword(tenant.getDatabasePassword());
hikariConfig.setDriverClassName("org.postgresql.Driver");
// Configure connection pool
ResourceLimits limits = tenant.getResourceLimits();
hikariConfig.setMaximumPoolSize(
limits != null && limits.getMaxDatabaseConnections() != null ?
limits.getMaxDatabaseConnections() : DEFAULT_MAX_POOL_SIZE);
hikariConfig.setMinimumIdle(2);
hikariConfig.setConnectionTimeout(30000);
hikariConfig.setIdleTimeout(600000);
hikariConfig.setMaxLifetime(1800000);
// Tenant-specific connection properties
hikariConfig.addDataSourceProperty("ApplicationName", 
"Tenant-" + tenantId);
hikariConfig.addDataSourceProperty("currentSchema", 
config.getProperty("schema", "public"));
this.dataSource = new HikariDataSource(hikariConfig);
// Test connection
try (Connection conn = dataSource.getConnection()) {
log.debug("Database connection established for tenant: {}", tenantId);
}
} catch (SQLException e) {
throw new CapsuleException("Failed to initialize data source", e);
}
}
@Override
protected void initializeExecutorService() throws CapsuleException {
ResourceLimits limits = tenant.getResourceLimits();
int maxThreads = limits != null && limits.getMaxMemoryMB() != null ?
limits.getMaxMemoryMB() / 10 : 5; // Heuristic based on memory
ThreadFactory threadFactory = new TenantThreadFactory(tenantId);
this.executorService = Executors.newFixedThreadPool(
Math.max(1, maxThreads), threadFactory);
}
@Override
protected void initializeCache() throws CapsuleException {
try {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheNames(Arrays.asList(
"users", "products", "orders", "config"
));
// Configure cache sizes based on tenant limits
long maxSize = tenant.getResourceLimits() != null &&
tenant.getResourceLimits().getMaxMemoryMB() != null ?
tenant.getResourceLimits().getMaxMemoryMB() * 1024L * 1024L / 4 : // 25% of memory for cache
10 * 1024 * 1024; // 10MB default
cacheManager.setCacheSpecification(
"maximumSize=" + (maxSize / (1024 * 1024)) + ",expireAfterWrite=1h");
this.cacheManager = cacheManager;
} catch (Exception e) {
throw new CapsuleException("Failed to initialize cache", e);
}
}
@Override
public CapsuleMetrics getMetrics() {
if (!initialized) {
return new CapsuleMetrics(0, 0, 0, 0.0, Instant.now());
}
try {
Runtime runtime = Runtime.getRuntime();
long memoryUsage = runtime.totalMemory() - runtime.freeMemory();
int activeConnections = 0;
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikari = (HikariDataSource) dataSource;
activeConnections = hikari.getHikariPoolMXBean().getActiveConnections();
}
ThreadPoolExecutor executor = (ThreadPoolExecutor) executorService;
int activeThreads = executor.getActiveCount();
// Simplified CPU usage (in real implementation, use proper monitoring)
double cpuUsage = ManagementFactory.getThreadMXBean()
.getThreadCpuTime(Thread.currentThread().getId()) / 1_000_000.0;
return new CapsuleMetrics(memoryUsage, activeConnections, 
activeThreads, cpuUsage, Instant.now());
} catch (Exception e) {
log.warn("Failed to collect metrics for tenant: {}", tenantId, e);
return new CapsuleMetrics(0, 0, 0, 0.0, Instant.now());
}
}
@Override
public HealthCheckResult healthCheck() {
if (!initialized) {
return new HealthCheckResult(false, "Capsule not initialized", Map.of());
}
Map<String, Object> details = new HashMap<>();
boolean healthy = true;
String message = "All checks passed";
try {
// Check database connection
try (Connection conn = dataSource.getConnection()) {
details.put("database", "connected");
} catch (SQLException e) {
healthy = false;
message = "Database connection failed";
details.put("database", "disconnected");
details.put("databaseError", e.getMessage());
}
// Check thread pool
ThreadPoolExecutor executor = (ThreadPoolExecutor) executorService;
details.put("threadPoolActive", executor.getActiveCount());
details.put("threadPoolQueueSize", executor.getQueue().size());
if (executor.isShutdown() || executor.isTerminated()) {
healthy = false;
message = "Thread pool is not running";
}
// Check memory
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
double memoryUsagePercent = (double) usedMemory / maxMemory * 100;
details.put("memoryUsedMB", usedMemory / (1024 * 1024));
details.put("memoryMaxMB", maxMemory / (1024 * 1024));
details.put("memoryUsagePercent", Math.round(memoryUsagePercent * 100.0) / 100.0);
if (memoryUsagePercent > 90) {
healthy = false;
message = "High memory usage";
}
} catch (Exception e) {
healthy = false;
message = "Health check failed with exception";
details.put("error", e.getMessage());
}
return new HealthCheckResult(healthy, message, details);
}
private static class TenantThreadFactory implements ThreadFactory {
private final String tenantId;
private final AtomicInteger threadNumber = new AtomicInteger(1);
public TenantThreadFactory(String tenantId) {
this.tenantId = tenantId;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("tenant-" + tenantId + "-thread-" + threadNumber.getAndIncrement());
thread.setDaemon(true);
return thread;
}
}
}

Capsule Manager

1. Capsule Registry
@Component
@Slf4j
public class CapsuleRegistry {
private final Map<String, TenantCapsule> capsules = new ConcurrentHashMap<>();
private final Map<String, Instant> lastAccess = new ConcurrentHashMap<>();
private final TenantCapsuleFactory capsuleFactory;
private final int maxCapsules;
private final long accessTimeoutMinutes;
public CapsuleRegistry(TenantCapsuleFactory capsuleFactory,
@Value("${app.multi-tenancy.capsule.max-tenants:100}") int maxCapsules,
@Value("${app.multi-tenancy.capsule.access-timeout-minutes:30}") long accessTimeoutMinutes) {
this.capsuleFactory = capsuleFactory;
this.maxCapsules = maxCapsules;
this.accessTimeoutMinutes = accessTimeoutMinutes;
startEvictionTask();
}
public TenantCapsule getCapsule(String tenantId) throws CapsuleException {
lastAccess.put(tenantId, Instant.now());
return capsules.computeIfAbsent(tenantId, id -> {
try {
if (capsules.size() >= maxCapsules) {
evictLeastRecentlyUsed();
}
TenantCapsule capsule = capsuleFactory.createCapsule(tenantId);
capsule.initialize();
return capsule;
} catch (Exception e) {
throw new RuntimeException("Failed to create capsule", e);
}
});
}
public boolean hasCapsule(String tenantId) {
return capsules.containsKey(tenantId);
}
public void evictCapsule(String tenantId) {
TenantCapsule capsule = capsules.remove(tenantId);
lastAccess.remove(tenantId);
if (capsule != null) {
try {
capsule.shutdown();
} catch (CapsuleException e) {
log.warn("Error shutting down capsule for tenant: {}", tenantId, e);
}
}
}
public List<String> getActiveTenants() {
return new ArrayList<>(capsules.keySet());
}
public Map<String, CapsuleMetrics> getMetrics() {
return capsules.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().getMetrics()
));
}
private void evictLeastRecentlyUsed() {
String tenantToEvict = lastAccess.entrySet().stream()
.min(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
if (tenantToEvict != null) {
log.info("Evicting least recently used capsule for tenant: {}", tenantToEvict);
evictCapsule(tenantToEvict);
}
}
private void startEvictionTask() {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(
r -> new Thread(r, "capsule-eviction-task"));
scheduler.scheduleAtFixedRate(this::evictIdleCapsules, 5, 5, TimeUnit.MINUTES);
}
private void evictIdleCapsules() {
Instant cutoff = Instant.now().minus(accessTimeoutMinutes, ChronoUnit.MINUTES);
lastAccess.entrySet().stream()
.filter(entry -> entry.getValue().isBefore(cutoff))
.map(Map.Entry::getKey)
.forEach(tenantId -> {
log.info("Evicting idle capsule for tenant: {}", tenantId);
evictCapsule(tenantId);
});
}
@PreDestroy
public void shutdown() {
log.info("Shutting down capsule registry");
capsules.keySet().forEach(this::evictCapsule);
}
}
2. Capsule Factory
@Component
@Slf4j
public class TenantCapsuleFactory {
private final TenantRepository tenantRepository;
private final MultiTenancyConfig multiTenancyConfig;
public TenantCapsuleFactory(TenantRepository tenantRepository,
MultiTenancyConfig multiTenancyConfig) {
this.tenantRepository = tenantRepository;
this.multiTenancyConfig = multiTenancyConfig;
}
public TenantCapsule createCapsule(String tenantId) throws CapsuleException {
Tenant tenant = tenantRepository.findById(tenantId)
.orElseThrow(() -> new CapsuleException("Tenant not found: " + tenantId));
if (tenant.getStatus() != TenantStatus.ACTIVE) {
throw new CapsuleException("Tenant is not active: " + tenantId);
}
switch (multiTenancyConfig.getStrategy()) {
case DATABASE:
return new DatabasePerTenantCapsule(tenant);
case SCHEMA:
return new SchemaPerTenantCapsule(tenant);
case DISCRIMINATOR:
return new DiscriminatorCapsule(tenant);
default:
throw new CapsuleException("Unsupported multi-tenancy strategy: " + 
multiTenancyConfig.getStrategy());
}
}
}

Spring Boot Integration

1. Multi-Tenancy Configuration
@Configuration
@EnableConfigurationProperties(MultiTenancyProperties.class)
public class MultiTenancyConfig {
@Bean
public CapsuleRegistry capsuleRegistry(TenantCapsuleFactory capsuleFactory,
MultiTenancyProperties properties) {
return new CapsuleRegistry(capsuleFactory,
properties.getCapsule().getMaxTenants(),
properties.getCapsule().getAccessTimeoutMinutes());
}
@Bean
public TenantCapsuleFactory tenantCapsuleFactory(TenantRepository tenantRepository,
MultiTenancyProperties properties) {
return new TenantCapsuleFactory(tenantRepository, properties);
}
@Bean
public TenantResolver tenantResolver() {
return new HeaderTenantResolver();
}
@Bean
public TenantFilter tenantFilter(TenantResolver tenantResolver,
CapsuleRegistry capsuleRegistry) {
return new TenantFilter(tenantResolver, capsuleRegistry);
}
}
@ConfigurationProperties(prefix = "app.multi-tenancy")
@Validated
public class MultiTenancyProperties {
private boolean enabled = true;
private MultiTenancyStrategy strategy = MultiTenancyStrategy.DATABASE;
private String defaultTenant = "default";
private CapsuleProperties capsule = new CapsuleProperties();
// Getters and setters
public static class CapsuleProperties {
private int maxTenants = 100;
private long accessTimeoutMinutes = 30;
private IsolationLevel isolationLevel = IsolationLevel.DATABASE;
private ResourceLimits resourceLimits = new ResourceLimits();
// Getters and setters
}
}
public enum MultiTenancyStrategy {
DATABASE, SCHEMA, DISCRIMINATOR
}
public enum IsolationLevel {
DATABASE, SCHEMA, THREAD, NONE
}
2. Tenant Resolution
public interface TenantResolver {
String resolveTenantId(HttpServletRequest request);
}
@Component
public class HeaderTenantResolver implements TenantResolver {
private static final String TENANT_HEADER = "X-Tenant-ID";
private static final String DEFAULT_TENANT = "default";
@Override
public String resolveTenantId(HttpServletRequest request) {
String tenantId = request.getHeader(TENANT_HEADER);
if (tenantId == null || tenantId.trim().isEmpty()) {
// Fallback to subdomain or other strategies
tenantId = resolveFromSubdomain(request);
}
return tenantId != null ? tenantId.trim() : DEFAULT_TENANT;
}
private String resolveFromSubdomain(HttpServletRequest request) {
String serverName = request.getServerName();
if (serverName.contains(".")) {
String subdomain = serverName.split("\\.")[0];
if (!subdomain.equals("www") && !subdomain.equals("app")) {
return subdomain;
}
}
return null;
}
}
@Component
public class JwtTenantResolver implements TenantResolver {
private static final String TENANT_CLAIM = "tenant_id";
@Override
public String resolveTenantId(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
return extractTenantFromToken(token);
}
return null;
}
private String extractTenantFromToken(String token) {
try {
// In production, use proper JWT library
String[] parts = token.split("\\.");
if (parts.length == 3) {
String payload = new String(Base64.getUrlDecoder().decode(parts[1]));
JsonNode jsonNode = new ObjectMapper().readTree(payload);
if (jsonNode.has(TENANT_CLAIM)) {
return jsonNode.get(TENANT_CLAIM).asText();
}
}
} catch (Exception e) {
// Log and continue
}
return null;
}
}
3. Tenant Filter
@Component
@Slf4j
public class TenantFilter implements Filter {
private final TenantResolver tenantResolver;
private final CapsuleRegistry capsuleRegistry;
public TenantFilter(TenantResolver tenantResolver, CapsuleRegistry capsuleRegistry) {
this.tenantResolver = tenantResolver;
this.capsuleRegistry = capsuleRegistry;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
String tenantId = tenantResolver.resolveTenantId(httpRequest);
if (tenantId == null) {
sendError(httpResponse, "Tenant identification required", 
HttpStatus.BAD_REQUEST);
return;
}
// Get or create capsule for tenant
TenantCapsule capsule;
try {
capsule = capsuleRegistry.getCapsule(tenantId);
} catch (CapsuleException e) {
log.error("Failed to get capsule for tenant: {}", tenantId, e);
sendError(httpResponse, "Tenant not available", 
HttpStatus.SERVICE_UNAVAILABLE);
return;
}
// Set tenant context
TenantContext.setCurrentTenant(tenantId);
TenantContext.setTenantConfig(capsule.getConfig());
// Add tenant info to response headers
httpResponse.setHeader("X-Tenant-ID", tenantId);
chain.doFilter(request, response);
} finally {
TenantContext.clear();
}
}
private void sendError(HttpServletResponse response, String message, HttpStatus status) 
throws IOException {
response.setStatus(status.value());
response.setContentType("application/json");
Map<String, Object> error = Map.of(
"error", status.getReasonPhrase(),
"message", message,
"status", status.value(),
"timestamp", Instant.now()
);
response.getWriter().write(new ObjectMapper().writeValueAsString(error));
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("Tenant filter initialized");
}
@Override
public void destroy() {
log.info("Tenant filter destroyed");
}
}
4. Tenant-Aware Services
@Service
@Slf4j
public class TenantAwareUserService {
private final CapsuleRegistry capsuleRegistry;
private final UserRepository userRepository;
public TenantAwareUserService(CapsuleRegistry capsuleRegistry,
UserRepository userRepository) {
this.capsuleRegistry = capsuleRegistry;
this.userRepository = userRepository;
}
@Transactional
public User createUser(UserCreateRequest request) {
String tenantId = TenantContext.getCurrentTenant();
if (tenantId == null) {
throw new TenantNotSetException("Tenant context not set");
}
// Check tenant resource limits
checkUserLimit(tenantId);
User user = new User();
user.setTenantId(tenantId);
user.setEmail(request.getEmail());
user.setName(request.getName());
user.setCreatedAt(Instant.now());
return userRepository.save(user);
}
public List<User> getUsers() {
String tenantId = TenantContext.getCurrentTenant();
if (tenantId == null) {
throw new TenantNotSetException("Tenant context not set");
}
return userRepository.findByTenantId(tenantId);
}
public User getUserById(String userId) {
String tenantId = TenantContext.getCurrentTenant();
return userRepository.findByTenantIdAndId(tenantId, userId)
.orElseThrow(() -> new UserNotFoundException("User not found: " + userId));
}
private void checkUserLimit(String tenantId) {
try {
TenantCapsule capsule = capsuleRegistry.getCapsule(tenantId);
ResourceLimits limits = capsule.getConfig().getResourceLimits();
if (limits != null && limits.getMaxUsers() != null) {
long currentUserCount = userRepository.countByTenantId(tenantId);
if (currentUserCount >= limits.getMaxUsers()) {
throw new ResourceLimitExceededException(
"User limit exceeded for tenant: " + tenantId);
}
}
} catch (CapsuleException e) {
log.warn("Failed to check user limit for tenant: {}", tenantId, e);
}
}
}
5. Tenant Repository with Data Isolation
@Repository
public interface UserRepository extends JpaRepository<User, String> {
@Query("SELECT u FROM User u WHERE u.tenantId = :tenantId")
List<User> findByTenantId(@Param("tenantId") String tenantId);
@Query("SELECT u FROM User u WHERE u.tenantId = :tenantId AND u.id = :id")
Optional<User> findByTenantIdAndId(@Param("tenantId") String tenantId, 
@Param("id") String id);
@Query("SELECT COUNT(u) FROM User u WHERE u.tenantId = :tenantId")
long countByTenantId(@Param("tenantId") String tenantId);
@Modifying
@Query("DELETE FROM User u WHERE u.tenantId = :tenantId AND u.id = :id")
void deleteByTenantIdAndId(@Param("tenantId") String tenantId, 
@Param("id") String id);
}
@Entity
@Table(name = "users")
public class User {
@Id
private String id;
@Column(nullable = false)
private String tenantId;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String name;
private Instant createdAt;
private Instant updatedAt;
@PrePersist
protected void onCreate() {
id = UUID.randomUUID().toString();
createdAt = Instant.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = Instant.now();
}
// Constructors, getters, setters
}

REST Controllers

1. Tenant Management Controller
@RestController
@RequestMapping("/api/tenants")
@Slf4j
public class TenantController {
private final CapsuleRegistry capsuleRegistry;
private final TenantService tenantService;
public TenantController(CapsuleRegistry capsuleRegistry, TenantService tenantService) {
this.capsuleRegistry = capsuleRegistry;
this.tenantService = tenantService;
}
@PostMapping
public ResponseEntity<Tenant> createTenant(@RequestBody @Valid TenantCreateRequest request) {
Tenant tenant = tenantService.createTenant(request);
return ResponseEntity.status(HttpStatus.CREATED).body(tenant);
}
@GetMapping("/{tenantId}/status")
public ResponseEntity<CapsuleStatus> getCapsuleStatus(@PathVariable String tenantId) {
try {
TenantCapsule capsule = capsuleRegistry.getCapsule(tenantId);
HealthCheckResult health = capsule.healthCheck();
CapsuleMetrics metrics = capsule.getMetrics();
CapsuleStatus status = new CapsuleStatus(
tenantId,
capsule.getStatus(),
health.isHealthy(),
health.getMessage(),
metrics,
health.getDetails()
);
return ResponseEntity.ok(status);
} catch (CapsuleException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
@PostMapping("/{tenantId}/evict")
public ResponseEntity<Void> evictCapsule(@PathVariable String tenantId) {
capsuleRegistry.evictCapsule(tenantId);
return ResponseEntity.ok().build();
}
@GetMapping("/metrics")
public ResponseEntity<Map<String, CapsuleMetrics>> getAllMetrics() {
Map<String, CapsuleMetrics> metrics = capsuleRegistry.getMetrics();
return ResponseEntity.ok(metrics);
}
public static class CapsuleStatus {
private final String tenantId;
private final TenantStatus status;
private final boolean healthy;
private final String healthMessage;
private final CapsuleMetrics metrics;
private final Map<String, Object> healthDetails;
// Constructor, getters
}
}
2. Business Controller
@RestController
@RequestMapping("/api/users")
@Slf4j
public class UserController {
private final TenantAwareUserService userService;
public UserController(TenantAwareUserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody @Valid UserCreateRequest request) {
User user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@GetMapping
public ResponseEntity<List<User>> getUsers() {
List<User> users = userService.getUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/{userId}")
public ResponseEntity<User> getUser(@PathVariable String userId) {
User user = userService.getUserById(userId);
return ResponseEntity.ok(user);
}
@GetMapping("/me")
public ResponseEntity<UserContext> getCurrentContext() {
String tenantId = TenantContext.getCurrentTenant();
TenantConfig config = TenantContext.getCurrentConfig();
UserContext context = new UserContext(tenantId, config);
return ResponseEntity.ok(context);
}
public static class UserContext {
private final String tenantId;
private final TenantConfig config;
private final Instant timestamp;
public UserContext(String tenantId, TenantConfig config) {
this.tenantId = tenantId;
this.config = config;
this.timestamp = Instant.now();
}
// Getters
}
}

Actuator Endpoints

@Component
@Endpoint(id = "tenants")
@Slf4j
public class TenantsActuatorEndpoint {
private final CapsuleRegistry capsuleRegistry;
private final TenantRepository tenantRepository;
public TenantsActuatorEndpoint(CapsuleRegistry capsuleRegistry,
TenantRepository tenantRepository) {
this.capsuleRegistry = capsuleRegistry;
this.tenantRepository = tenantRepository;
}
@ReadOperation
public TenantSummary getTenantSummary() {
List<String> activeTenants = capsuleRegistry.getActiveTenants();
Map<String, CapsuleMetrics> metrics = capsuleRegistry.getMetrics();
long totalTenants = tenantRepository.count();
long activeTenantsCount = activeTenants.size();
Map<String, Object> details = new HashMap<>();
details.put("activeTenants", activeTenants);
details.put("metrics", metrics);
details.put("totalRegisteredTenants", totalTenants);
return new TenantSummary(activeTenantsCount, totalTenants, details);
}
@WriteOperation
public OperationResult evictTenant(@Selector String tenantId) {
try {
capsuleRegistry.evictCapsule(tenantId);
return new OperationResult(true, "Tenant evicted: " + tenantId);
} catch (Exception e) {
return new OperationResult(false, "Failed to evict tenant: " + e.getMessage());
}
}
public static class TenantSummary {
private final long activeTenants;
private final long totalTenants;
private final Map<String, Object> details;
// Constructor, getters
}
public static class OperationResult {
private final boolean success;
private final String message;
private final Instant timestamp;
// Constructor, getters
}
}

Testing

1. Integration Tests
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
class CapsuleMultiTenancyTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
private CapsuleRegistry capsuleRegistry;
@Autowired
private TenantAwareUserService userService;
@Test
void testTenantIsolation() {
// Set tenant context
TenantContext.setCurrentTenant("tenant1");
UserCreateRequest request = new UserCreateRequest("[email protected]", "User One");
User user1 = userService.createUser(request);
// Switch to different tenant
TenantContext.setCurrentTenant("tenant2");
UserCreateRequest request2 = new UserCreateRequest("[email protected]", "User Two");
User user2 = userService.createUser(request2);
// Verify tenants can only see their own data
TenantContext.setCurrentTenant("tenant1");
List<User> tenant1Users = userService.getUsers();
assertThat(tenant1Users).hasSize(1);
assertThat(tenant1Users.get(0).getEmail()).isEqualTo("[email protected]");
TenantContext.setCurrentTenant("tenant2");
List<User> tenant2Users = userService.getUsers();
assertThat(tenant2Users).hasSize(1);
assertThat(tenant2Users.get(0).getEmail()).isEqualTo("[email protected]");
}
@Test
void testCapsuleLifecycle() throws CapsuleException {
TenantCapsule capsule = capsuleRegistry.getCapsule("test-tenant");
assertThat(capsule).isNotNull();
assertThat(capsule.isInitialized()).isTrue();
assertThat(capsule.healthCheck().isHealthy()).isTrue();
// Evict and verify shutdown
capsuleRegistry.evictCapsule("test-tenant");
assertThat(capsuleRegistry.hasCapsule("test-tenant")).isFalse();
}
}

Best Practices

  1. Resource Limits: Always enforce resource limits per tenant
  2. Isolation: Choose appropriate isolation level based on security requirements
  3. Monitoring: Implement comprehensive monitoring for each capsule
  4. Eviction Strategy: Implement LRU or time-based eviction for resource management
  5. Error Handling: Provide clear error messages for tenant-specific issues
  6. Testing: Test cross-tenant data isolation thoroughly
// Example of tenant resource monitoring
@Component
@Slf4j
public class TenantResourceMonitor {
private final CapsuleRegistry capsuleRegistry;
private final MeterRegistry meterRegistry;
@Scheduled(fixedRate = 60000) // Every minute
public void monitorTenantResources() {
capsuleRegistry.getMetrics().forEach((tenantId, metrics) -> {
// Record metrics to monitoring system
gaugeTenantMemoryUsage(tenantId, metrics.getMemoryUsage());
gaugeTenantConnections(tenantId, metrics.getActiveConnections());
gaugeTenantThreads(tenantId, metrics.getActiveThreads());
});
}
private void gaugeTenantMemoryUsage(String tenantId, long memoryUsage) {
meterRegistry.gauge("tenant.memory.usage", 
Tags.of("tenant", tenantId), 
memoryUsage);
}
// Other gauge methods...
}

Conclusion

Capsule-based multi-tenancy provides:

  • Strong isolation between tenants with dedicated resources
  • Dynamic resource management with automatic scaling and eviction
  • Unified administration through centralized capsule management
  • Flexible deployment supporting multiple isolation strategies

This approach enables building scalable, secure multi-tenant applications that can efficiently serve hundreds of tenants while maintaining strong isolation and resource control. The capsule pattern abstracts away the complexity of multi-tenancy, allowing developers to focus on business logic while the framework handles tenant isolation and resource management.

Java Logistics, Shipping Integration & Enterprise Inventory Automation (Tracking, ERP, RFID & Billing Systems)

https://macronepal.com/blog/aftership-tracking-in-java-enterprise-package-visibility/
Explains how to integrate AfterShip tracking services into Java applications to provide real-time shipment visibility, delivery status updates, and centralized tracking across multiple courier services.

https://macronepal.com/blog/shipping-integration-using-fedex-api-with-java-for-logistics-automation/
Explains how to integrate the FedEx API into Java systems to automate shipping tasks such as creating shipments, calculating delivery costs, generating shipping labels, and tracking packages.

https://macronepal.com/blog/shipping-and-logistics-integrating-ups-apis-with-java-applications/
Explains UPS API integration in Java to enable automated shipping operations including rate calculation, shipment scheduling, tracking, and delivery confirmation management.

https://macronepal.com/blog/generating-and-reading-qr-codes-for-products-in-java/
Explains how Java applications generate and read QR codes for product identification, tracking, and authentication, supporting faster inventory handling and product verification processes.

https://macronepal.com/blog/designing-a-robust-pick-and-pack-workflow-in-java/
Explains how to design an efficient pick-and-pack workflow in Java warehouse systems, covering order processing, item selection, packaging steps, and logistics preparation to improve fulfillment efficiency.

https://macronepal.com/blog/rfid-inventory-management-system-in-java-a-complete-guide/
Explains how RFID technology integrates with Java applications to automate inventory tracking, reduce manual errors, and enable real-time stock monitoring in warehouses and retail environments.

https://macronepal.com/blog/erp-integration-with-odoo-in-java/
Explains how Java applications connect with Odoo ERP systems to synchronize inventory, orders, customer records, and financial data across enterprise systems.

https://macronepal.com/blog/automated-invoice-generation-creating-professional-excel-invoices-with-apache-poi-in-java/
Explains how to automatically generate professional Excel invoices in Java using Apache POI, enabling structured billing documents and automated financial record creation.

https://macronepal.com/blog/enterprise-financial-integration-using-quickbooks-api-in-java-applications/
Explains QuickBooks API integration in Java to automate financial workflows such as invoice management, payment tracking, accounting synchronization, and financial reporting.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper