Multi-Realm Architecture Patterns

Multi-Realm Architecture Patterns

Multi-realm architecture in Keycloak enables true multi-tenancy with complete isolation between tenants. This tutorial explores patterns, implementation strategies, and best practices for building scalable multi-tenant applications.

Understanding Multi-Realm Architecture

What Are Realms?

Realms in Keycloak are:

  • Complete isolation boundaries - Users, clients, and settings are separate
  • Independent security domains - Each realm has its own configuration
  • Scalable units - Can be distributed across clusters
  • Perfect for multi-tenancy - Each tenant gets their own realm

When to Use Multiple Realms

Use multiple realms when you need:

  • Complete tenant isolation - No data sharing between tenants
  • Different authentication flows - Per-tenant customization
  • Separate branding - Each tenant’s look and feel
  • Independent user bases - No shared users
  • Compliance requirements - Data residency per tenant

Organizations in Keycloak

New Alternative: Keycloak Organizations

Keycloak now provides built-in organization management features that can simplify multi-tenancy without requiring separate realms:

Organizations allow you to:

  • Manage multiple tenants in a single realm - Reduces overhead and complexity
  • Group users by organization - Automatic user segmentation
  • Delegate administration - Organization-specific admins
  • Share realm configuration - Consistent settings across tenants
  • Implement organization-specific branding - Per-org customization

When to use Organizations instead of multiple realms:

  • When tenants can share the same authentication flows
  • When you need easier cross-tenant features
  • When resource efficiency is important
  • When tenants don’t require complete isolation

Learn more: Keycloak Organizations Documentation

Architecture Patterns

Pattern 1: Realm per Tenant

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Tenant A      │     │   Tenant B      │     │   Tenant C      │
│   Realm         │     │   Realm         │     │   Realm         │
├─────────────────┤     ├─────────────────┤     ├─────────────────┤
│ • Users         │     │ • Users         │     │ • Users         │
│ • Clients       │     │ • Clients       │     │ • Clients       │
│ • Roles         │     │ • Roles         │     │ • Roles         │
│ • Settings      │     │ • Settings      │     │ • Settings      │
└─────────────────┘     └─────────────────┘     └─────────────────┘

Pros:

  • Complete isolation
  • Per-tenant customization
  • Independent scaling
  • Clear security boundaries

Cons:

  • More resources required
  • Complex cross-tenant features
  • Realm management overhead

Pattern 2: Shared Services Realm

┌─────────────────────────────────────┐
│        Shared Services Realm        │
│  (Authentication, Common Services)  │
└─────────────────────────────────────┘
                  │
    ┌─────────────┼─────────────┐
    ▼             ▼             ▼
┌─────────┐   ┌─────────┐   ┌─────────┐
│ Tenant A│   │ Tenant B│   │ Tenant C│
│  Realm  │   │  Realm  │   │  Realm  │
└─────────┘   └─────────┘   └─────────┘

Use for:

  • Shared administrative users
  • Common services authentication
  • Cross-tenant features

Pattern 3: Hierarchical Realms

┌─────────────────────┐
│    Master Realm     │
├─────────────────────┤
│  Global Settings    │
│  Realm Templates    │
└──────────┬──────────┘
           │
    ┌──────┴──────┐
    ▼             ▼
┌─────────┐   ┌─────────┐
│Regional │   │Regional │
│ Realm 1 │   │ Realm 2 │
└────┬────┘   └────┬────┘
     │             │
  ┌──┴──┐      ┌──┴──┐
  ▼     ▼      ▼     ▼
┌────┐┌────┐ ┌────┐┌────┐
│Ten1││Ten2│ │Ten3││Ten4│
└────┘└────┘ └────┘└────┘

Implementation Guide

Step 1: Realm Management Service

@Service
public class RealmManagementService {
    
    private final Keycloak keycloakAdmin;
    private final RealmTemplateService templateService;
    
    public RealmManagementService(KeycloakProperties props) {
        this.keycloakAdmin = KeycloakBuilder.builder()
            .serverUrl(props.getServerUrl())
            .realm("master")
            .clientId("admin-cli")
            .clientSecret(props.getAdminSecret())
            .build();
    }
    
    public RealmRepresentation createTenantRealm(TenantConfig config) {
        try {
            // Generate unique realm name
            String realmName = generateRealmName(config.getTenantId());
            
            // Get base template
            RealmRepresentation realm = templateService.getTemplate(config.getPlan());
            
            // Customize for tenant
            realm.setRealm(realmName);
            realm.setDisplayName(config.getCompanyName());
            realm.setEnabled(true);
            
            // Set realm-specific settings
            customizeRealmSettings(realm, config);
            
            // Create realm
            keycloakAdmin.realms().create(realm);
            
            // Post-creation setup
            setupRealmDefaults(realmName, config);
            
            return realm;
            
        } catch (Exception e) {
            throw new RealmCreationException("Failed to create realm", e);
        }
    }
    
    private void customizeRealmSettings(RealmRepresentation realm, TenantConfig config) {
        // Security settings
        realm.setPasswordPolicy(getPasswordPolicyForPlan(config.getPlan()));
        realm.setBruteForceProtected(true);
        realm.setPermanentLockout(false);
        realm.setMaxFailureWaitSeconds(900);
        realm.setMaxDeltaTimeSeconds(43200);
        
        // Token settings
        realm.setAccessTokenLifespan(config.getTokenLifespan());
        realm.setSsoSessionMaxLifespan(config.getSessionLifespan());
        
        // Feature flags
        Map<String, String> attributes = new HashMap<>();
        attributes.put("plan", config.getPlan());
        attributes.put("features", String.join(",", config.getEnabledFeatures()));
        realm.setAttributes(attributes);
        
        // Branding
        if (config.getTheme() != null) {
            realm.setLoginTheme(config.getTheme());
            realm.setEmailTheme(config.getTheme());
        }
    }
    
    private void setupRealmDefaults(String realmName, TenantConfig config) {
        RealmResource realm = keycloakAdmin.realm(realmName);
        
        // Create default roles
        createDefaultRoles(realm);
        
        // Create default groups
        createDefaultGroups(realm);
        
        // Create service accounts
        createServiceAccounts(realm, config);
        
        // Configure identity providers
        if (config.getIdentityProviders() != null) {
            configureIdentityProviders(realm, config.getIdentityProviders());
        }
        
        // Set up event listeners
        configureEventListeners(realm);
    }
    
    private void createDefaultRoles(RealmResource realm) {
        List<RoleRepresentation> defaultRoles = Arrays.asList(
            createRole("tenant-admin", "Tenant Administrator"),
            createRole("tenant-user", "Regular User"),
            createRole("tenant-viewer", "Read-only User")
        );
        
        defaultRoles.forEach(role -> 
            realm.roles().create(role)
        );
    }
    
    private RoleRepresentation createRole(String name, String description) {
        RoleRepresentation role = new RoleRepresentation();
        role.setName(name);
        role.setDescription(description);
        role.setComposite(false);
        return role;
    }
}

Step 2: Dynamic Realm Routing

@Component
public class MultiRealmAuthenticationFilter extends OncePerRequestFilter {
    
    private final RealmResolver realmResolver;
    private final TokenValidator tokenValidator;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain filterChain) throws ServletException, IOException {
        
        try {
            // Resolve realm from request
            String realm = realmResolver.resolveRealm(request);
            
            if (realm == null) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unable to determine realm");
                return;
            }
            
            // Set realm context
            RealmContext.setCurrentRealm(realm);
            
            // Validate token for specific realm
            String token = extractToken(request);
            if (token != null) {
                DecodedJWT jwt = tokenValidator.validateTokenForRealm(token, realm);
                SecurityContext context = createSecurityContext(jwt, realm);
                SecurityContextHolder.setContext(context);
            }
            
            filterChain.doFilter(request, response);
            
        } finally {
            // Clean up
            RealmContext.clear();
            SecurityContextHolder.clearContext();
        }
    }
}

@Component
public class RealmResolver {
    
    public String resolveRealm(HttpServletRequest request) {
        // Strategy 1: Subdomain-based
        String host = request.getServerName();
        if (host.endsWith(".myapp.com")) {
            return host.substring(0, host.indexOf(".myapp.com"));
        }
        
        // Strategy 2: Path-based
        String path = request.getRequestURI();
        if (path.startsWith("/tenants/")) {
            String[] parts = path.split("/");
            if (parts.length > 2) {
                return parts[2];
            }
        }
        
        // Strategy 3: Header-based
        String realmHeader = request.getHeader("X-Tenant-Realm");
        if (realmHeader != null) {
            return realmHeader;
        }
        
        // Strategy 4: JWT claim
        String token = extractToken(request);
        if (token != null) {
            try {
                DecodedJWT jwt = JWT.decode(token);
                return jwt.getClaim("realm").asString();
            } catch (Exception e) {
                // Invalid token, try other strategies
            }
        }
        
        return null;
    }
}

Step 3: Cross-Realm Communication

@Service
public class CrossRealmService {
    
    private final Map<String, Keycloak> realmClients = new ConcurrentHashMap<>();
    
    public void executeInRealm(String realm, Consumer<RealmResource> action) {
        Keycloak client = getOrCreateClient(realm);
        RealmResource realmResource = client.realm(realm);
        
        try {
            action.accept(realmResource);
        } catch (Exception e) {
            throw new CrossRealmException("Failed to execute in realm: " + realm, e);
        }
    }
    
    public <T> T queryAcrossRealms(List<String> realms, 
                                  Function<RealmResource, T> query,
                                  BinaryOperator<T> combiner) {
        return realms.parallelStream()
            .map(realm -> {
                try {
                    Keycloak client = getOrCreateClient(realm);
                    return query.apply(client.realm(realm));
                } catch (Exception e) {
                    logger.error("Failed to query realm: " + realm, e);
                    return null;
                }
            })
            .filter(Objects::nonNull)
            .reduce(combiner)
            .orElse(null);
    }
    
    // Example: Count users across all realms
    public long getTotalUserCount(List<String> realms) {
        return queryAcrossRealms(
            realms,
            realm -> (long) realm.users().count(),
            Long::sum
        );
    }
    
    // Example: Search users across realms
    public List<UserRepresentation> searchUsersAcrossRealms(String search) {
        List<String> realms = getAllTenantRealms();
        
        return realms.parallelStream()
            .flatMap(realm -> {
                try {
                    return getOrCreateClient(realm)
                        .realm(realm)
                        .users()
                        .search(search, 0, 10)
                        .stream()
                        .map(user -> {
                            user.setAttributes(
                                Map.of("source_realm", realm)
                            );
                            return user;
                        });
                } catch (Exception e) {
                    return Stream.empty();
                }
            })
            .collect(Collectors.toList());
    }
}

Step 4: Realm Templates

@Component
public class RealmTemplateService {
    
    private final Map<String, RealmTemplate> templates = new HashMap<>();
    
    @PostConstruct
    public void initializeTemplates() {
        templates.put("starter", createStarterTemplate());
        templates.put("professional", createProfessionalTemplate());
        templates.put("enterprise", createEnterpriseTemplate());
    }
    
    private RealmTemplate createStarterTemplate() {
        return RealmTemplate.builder()
            .name("Starter Plan Template")
            .passwordPolicy("length(8) and specialChars(1) and digits(1)")
            .maxUsers(1000)
            .maxClients(5)
            .features(Arrays.asList("basic-auth", "social-login"))
            .tokenLifespan(300) // 5 minutes
            .sessionLifespan(1800) // 30 minutes
            .build();
    }
    
    private RealmTemplate createEnterpriseTemplate() {
        return RealmTemplate.builder()
            .name("Enterprise Plan Template")
            .passwordPolicy("length(12) and specialChars(2) and digits(2) and upperCase(1)")
            .maxUsers(-1) // Unlimited
            .maxClients(-1) // Unlimited
            .features(Arrays.asList(
                "basic-auth", 
                "social-login", 
                "mfa", 
                "user-federation",
                "custom-flows",
                "advanced-analytics"
            ))
            .tokenLifespan(900) // 15 minutes
            .sessionLifespan(28800) // 8 hours
            .identityProviders(Arrays.asList(
                createSAMLProvider(),
                createOIDCProvider()
            ))
            .customAuthFlows(Arrays.asList(
                "progressive-profiling",
                "risk-based-auth"
            ))
            .build();
    }
    
    public RealmRepresentation applyTemplate(String templateName, String realmName) {
        RealmTemplate template = templates.get(templateName);
        if (template == null) {
            throw new IllegalArgumentException("Unknown template: " + templateName);
        }
        
        RealmRepresentation realm = new RealmRepresentation();
        realm.setRealm(realmName);
        realm.setEnabled(true);
        
        // Apply template settings
        realm.setPasswordPolicy(template.getPasswordPolicy());
        realm.setAccessTokenLifespan(template.getTokenLifespan());
        realm.setSsoSessionMaxLifespan(template.getSessionLifespan());
        
        // Set attributes for feature flags
        Map<String, String> attributes = new HashMap<>();
        attributes.put("max_users", String.valueOf(template.getMaxUsers()));
        attributes.put("max_clients", String.valueOf(template.getMaxClients()));
        attributes.put("enabled_features", String.join(",", template.getFeatures()));
        attributes.put("template", templateName);
        realm.setAttributes(attributes);
        
        return realm;
    }
}

Step 5: Monitoring and Management

@RestController
@RequestMapping("/api/admin/realms")
public class RealmManagementController {
    
    private final RealmManagementService realmService;
    private final RealmMetricsService metricsService;
    
    @GetMapping
    public ResponseEntity<List<RealmInfo>> listRealms(
            @RequestParam(required = false) String status,
            @RequestParam(required = false) String plan) {
        
        List<RealmInfo> realms = realmService.getAllRealms().stream()
            .filter(realm -> status == null || realm.getStatus().equals(status))
            .filter(realm -> plan == null || realm.getPlan().equals(plan))
            .map(this::enrichWithMetrics)
            .collect(Collectors.toList());
        
        return ResponseEntity.ok(realms);
    }
    
    @GetMapping("/{realmId}/metrics")
    public ResponseEntity<RealmMetrics> getRealmMetrics(@PathVariable String realmId) {
        RealmMetrics metrics = metricsService.getMetrics(realmId);
        return ResponseEntity.ok(metrics);
    }
    
    @PostMapping("/{realmId}/suspend")
    public ResponseEntity<Void> suspendRealm(@PathVariable String realmId) {
        realmService.suspendRealm(realmId);
        return ResponseEntity.noContent().build();
    }
    
    @DeleteMapping("/{realmId}")
    public ResponseEntity<Void> deleteRealm(
            @PathVariable String realmId,
            @RequestParam(defaultValue = "false") boolean immediate) {
        
        if (immediate) {
            realmService.deleteRealmImmediately(realmId);
        } else {
            realmService.scheduleRealmDeletion(realmId, 30); // 30 days
        }
        
        return ResponseEntity.noContent().build();
    }
    
    private RealmInfo enrichWithMetrics(RealmInfo realm) {
        try {
            RealmMetrics metrics = metricsService.getQuickMetrics(realm.getId());
            realm.setUserCount(metrics.getUserCount());
            realm.setActiveUsers(metrics.getActiveUsers());
            realm.setStorageUsed(metrics.getStorageUsed());
        } catch (Exception e) {
            logger.warn("Failed to get metrics for realm: " + realm.getId(), e);
        }
        return realm;
    }
}

@Service
public class RealmMetricsService {
    
    private final MeterRegistry meterRegistry;
    private final Cache<String, RealmMetrics> metricsCache;
    
    @Scheduled(fixedDelay = 60000) // Every minute
    public void collectMetrics() {
        List<String> realms = getAllRealmIds();
        
        realms.parallelStream().forEach(realmId -> {
            try {
                RealmMetrics metrics = collectRealmMetrics(realmId);
                metricsCache.put(realmId, metrics);
                
                // Update Prometheus metrics
                updatePrometheusMetrics(realmId, metrics);
            } catch (Exception e) {
                logger.error("Failed to collect metrics for realm: " + realmId, e);
            }
        });
    }
    
    private void updatePrometheusMetrics(String realmId, RealmMetrics metrics) {
        meterRegistry.gauge("keycloak_realm_users", 
            Tags.of("realm", realmId), metrics.getUserCount());
        
        meterRegistry.gauge("keycloak_realm_active_sessions", 
            Tags.of("realm", realmId), metrics.getActiveSessions());
        
        meterRegistry.gauge("keycloak_realm_storage_bytes", 
            Tags.of("realm", realmId), metrics.getStorageUsed());
    }
}

Managing Branding Across Multiple Realms

Consistent Branding Strategy

When managing multiple realms, maintaining consistent branding is crucial for user experience:

Manual Approach (Traditional):

  • Configure each realm individually
  • Maintain separate theme deployments
  • Risk of inconsistencies
  • Time-consuming updates

Automated Approach (Skycloak): Skycloak provides the “Apply to All Realms” feature for efficient branding management:

  1. Configure Primary Realm:

    • Set up branding on your main realm
    • Test and verify appearance
    • Ensure all settings are correct
    • Important: Click “Apply Branding” to save changes
  2. Apply to All Realms:

    • The button is only enabled after changes are saved
    • Click “Apply to All Realms” button
    • Select components to copy:
      • Visual branding (logos, colors)
      • Custom themes
      • Login settings
    • Exclude specific realms if needed

Workflow Requirement: You must apply pending changes to the current realm before using “Apply to All Realms”. The button will be disabled and show a tooltip if you have unsaved changes. This ensures the source realm has the latest configuration before propagating to other realms.

  1. How It Works:
    • Reads theme names from source realm’s Keycloak configuration
    • Applies exact theme names to target realms
    • Copies database configuration for UI consistency
    • No redeployment needed - themes already exist in container

Benefits:

  • Consistency: All realms have identical branding
  • Efficiency: Update multiple realms in one operation
  • Reliability: Direct theme name copying prevents errors
  • Speed: No theme file deployments required

Branding Patterns for Multi-Realm

Pattern 1: Uniform Branding All realms share the same branding:

Main Realm (configured) → Apply to All → All realms identical

Use when: Single brand across all tenants

Pattern 2: Tiered Branding Different branding per tier:

Premium Realm → Apply to Premium Group
Standard Realm → Apply to Standard Group

Use when: Different service tiers need distinction

Pattern 3: White-Label Branding Each realm has unique branding:

Configure each realm individually
No "Apply to All" usage

Use when: Each tenant needs complete customization

Best Practices

  1. Test First: Always test branding on a single realm before applying to all
  2. Use Exclusions: Exclude development/test realms from bulk updates
  3. Version Control: Keep theme files in version control for rollback
  4. Document Changes: Track which realms have which branding version
  5. Monitor Application: Check logs during bulk application for any failures

Performance Optimization

Realm Caching Strategy

@Configuration
public class RealmCacheConfig {
    
    @Bean
    public CacheManager realmCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(Duration.ofMinutes(5))
            .recordStats());
        return cacheManager;
    }
    
    @Bean
    public RealmCache realmCache() {
        return new RealmCache() {
            private final LoadingCache<String, RealmData> cache = Caffeine.newBuilder()
                .maximumSize(500)
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .refreshAfterWrite(2, TimeUnit.MINUTES)
                .build(this::loadRealm);
            
            private RealmData loadRealm(String realmId) {
                // Load from Keycloak
                return keycloakService.getRealmData(realmId);
            }
            
            @Override
            public RealmData get(String realmId) {
                return cache.get(realmId);
            }
            
            @Override
            public void invalidate(String realmId) {
                cache.invalidate(realmId);
            }
        };
    }
}

Connection Pooling

@Configuration
public class KeycloakConnectionConfig {
    
    @Bean
    public KeycloakConnectionPool connectionPool() {
        return new KeycloakConnectionPool(
            PoolConfig.builder()
                .maxTotal(100)
                .maxPerRoute(10)
                .connectionTimeout(Duration.ofSeconds(5))
                .socketTimeout(Duration.ofSeconds(30))
                .build()
        );
    }
    
    @Bean
    public KeycloakClientFactory clientFactory(KeycloakConnectionPool pool) {
        return new KeycloakClientFactory() {
            private final Map<String, Keycloak> clients = new ConcurrentHashMap<>();
            
            @Override
            public Keycloak getClient(String realm) {
                return clients.computeIfAbsent(realm, r -> 
                    createPooledClient(r, pool)
                );
            }
            
            private Keycloak createPooledClient(String realm, KeycloakConnectionPool pool) {
                return KeycloakBuilder.builder()
                    .serverUrl(keycloakUrl)
                    .realm(realm)
                    .clientId(clientId)
                    .clientSecret(clientSecret)
                    .resteasyClient(pool.getClient())
                    .build();
            }
        };
    }
}

Security Considerations

Realm Isolation Enforcement

@Aspect
@Component
public class RealmIsolationAspect {
    
    @Around("@annotation(RealmIsolated)")
    public Object enforceRealmIsolation(ProceedingJoinPoint joinPoint) throws Throwable {
        String currentRealm = RealmContext.getCurrentRealm();
        
        if (currentRealm == null) {
            throw new SecurityException("No realm context set");
        }
        
        // Extract realm from method arguments
        Object[] args = joinPoint.getArgs();
        String targetRealm = extractTargetRealm(args);
        
        if (targetRealm != null && !targetRealm.equals(currentRealm)) {
            throw new SecurityException(
                "Access denied: Cannot access realm " + targetRealm + 
                " from realm " + currentRealm
            );
        }
        
        return joinPoint.proceed();
    }
}

// Usage
@Service
public class UserService {
    
    @RealmIsolated
    public User getUser(String realmId, String userId) {
        // Method can only access users from current realm
        return userRepository.findByRealmAndId(realmId, userId);
    }
}

Audit Logging

@Component
public class RealmAuditLogger {
    
    @EventListener
    public void handleRealmEvent(RealmEvent event) {
        AuditEntry entry = AuditEntry.builder()
            .timestamp(Instant.now())
            .eventType(event.getType())
            .realmId(event.getRealmId())
            .userId(event.getUserId())
            .details(event.getDetails())
            .ipAddress(event.getIpAddress())
            .userAgent(event.getUserAgent())
            .build();
        
        // Store in realm-specific audit log
        auditRepository.save(entry);
        
        // Alert on suspicious activity
        if (isSuspicious(event)) {
            alertService.sendSecurityAlert(entry);
        }
    }
    
    private boolean isSuspicious(RealmEvent event) {
        return event.getType() == EventType.REALM_DELETED ||
               event.getType() == EventType.MASS_USER_DELETION ||
               event.getType() == EventType.ADMIN_PERMISSION_CHANGED;
    }
}

Best Practices

1. Realm Naming Convention

public class RealmNamingStrategy {
    
    public String generateRealmName(TenantInfo tenant) {
        // Format: environment-region-tenantId
        return String.format("%s-%s-%s",
            tenant.getEnvironment().toLowerCase(),
            tenant.getRegion().toLowerCase(),
            tenant.getId().toLowerCase()
        );
    }
    
    // Examples:
    // prod-us-acme-corp
    // staging-eu-test-company
    // dev-asia-demo-tenant
}

2. Realm Limits and Quotas

@Component
public class RealmQuotaEnforcer {
    
    @Before("@annotation(RequiresQuotaCheck)")
    public void enforceQuota(JoinPoint joinPoint) {
        String realm = RealmContext.getCurrentRealm();
        RealmQuota quota = quotaService.getQuota(realm);
        
        // Check various limits
        if (isCreatingUser(joinPoint) && quota.isUserLimitReached()) {
            throw new QuotaExceededException("User limit reached for realm");
        }
        
        if (isCreatingClient(joinPoint) && quota.isClientLimitReached()) {
            throw new QuotaExceededException("Client limit reached for realm");
        }
    }
}

3. Backup and Recovery

@Service
public class RealmBackupService {
    
    @Scheduled(cron = "0 0 2 * * *") // 2 AM daily
    public void backupAllRealms() {
        List<String> realms = realmService.getAllRealmIds();
        
        realms.parallelStream().forEach(realmId -> {
            try {
                RealmRepresentation export = exportRealm(realmId);
                String backupPath = storeBackup(realmId, export);
                
                logger.info("Backed up realm {} to {}", realmId, backupPath);
            } catch (Exception e) {
                logger.error("Failed to backup realm: " + realmId, e);
            }
        });
    }
    
    public void restoreRealm(String realmId, String backupId) {
        RealmRepresentation backup = loadBackup(realmId, backupId);
        
        // Delete existing realm
        keycloak.realm(realmId).remove();
        
        // Recreate from backup
        keycloak.realms().create(backup);
        
        logger.info("Restored realm {} from backup {}", realmId, backupId);
    }
}

Testing Multi-Realm Applications

@SpringBootTest
public class MultiRealmIntegrationTest {
    
    @Autowired
    private RealmManagementService realmService;
    
    private List<String> testRealms = new ArrayList<>();
    
    @BeforeEach
    public void setupTestRealms() {
        // Create test realms
        for (int i = 0; i < 3; i++) {
            String realmId = "test-realm-" + i;
            realmService.createRealm(realmId);
            testRealms.add(realmId);
        }
    }
    
    @AfterEach
    public void cleanupTestRealms() {
        testRealms.forEach(realm -> {
            try {
                realmService.deleteRealm(realm);
            } catch (Exception e) {
                // Ignore
            }
        });
    }
    
    @Test
    public void testCrossRealmIsolation() {
        // Create user in realm 1
        String userId = createUser(testRealms.get(0), "user1");
        
        // Try to access from realm 2 - should fail
        RealmContext.setCurrentRealm(testRealms.get(1));
        
        assertThrows(SecurityException.class, () -> {
            userService.getUser(testRealms.get(0), userId);
        });
    }
    
    @Test
    public void testRealmSpecificConfiguration() {
        // Configure different settings per realm
        testRealms.forEach(realm -> {
            RealmSettings settings = new RealmSettings();
            settings.setPasswordPolicy("length(" + (8 + testRealms.indexOf(realm)) + ")");
            realmService.updateSettings(realm, settings);
        });
        
        // Verify each realm has its own settings
        for (int i = 0; i < testRealms.size(); i++) {
            RealmSettings settings = realmService.getSettings(testRealms.get(i));
            assertEquals("length(" + (8 + i) + ")", settings.getPasswordPolicy());
        }
    }
}

Next Steps