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
- Resource Limits: Always enforce resource limits per tenant
- Isolation: Choose appropriate isolation level based on security requirements
- Monitoring: Implement comprehensive monitoring for each capsule
- Eviction Strategy: Implement LRU or time-based eviction for resource management
- Error Handling: Provide clear error messages for tenant-specific issues
- 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.