|
| 1 | +# Multi-Tenancy Support Implementation Guide |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Comprehensive multi-tenancy support for IRCTC microservices using the **Shared Database, Shared Schema** approach. This implementation provides tenant isolation, tenant context propagation, and tenant management capabilities. |
| 6 | + |
| 7 | +## Architecture |
| 8 | + |
| 9 | +### Approach: Shared Database, Shared Schema |
| 10 | + |
| 11 | +- **Single Database**: All tenants share the same database |
| 12 | +- **Shared Schema**: All tenants use the same table structure |
| 13 | +- **Tenant Isolation**: Data is isolated using `tenant_id` column |
| 14 | +- **Row-Level Filtering**: Hibernate filters automatically filter data by tenant |
| 15 | + |
| 16 | +### Benefits |
| 17 | + |
| 18 | +- ✅ Cost-effective (single database) |
| 19 | +- ✅ Easy to maintain |
| 20 | +- ✅ Simple backup/restore |
| 21 | +- ✅ Good performance with proper indexing |
| 22 | +- ✅ Easy to scale |
| 23 | + |
| 24 | +## Components |
| 25 | + |
| 26 | +### 1. Tenant Entity |
| 27 | + |
| 28 | +**Location**: `Tenant.java` |
| 29 | + |
| 30 | +Represents a tenant in the system: |
| 31 | +- `code`: Unique tenant identifier (e.g., "acme-corp") |
| 32 | +- `name`: Tenant display name |
| 33 | +- `status`: ACTIVE, SUSPENDED, INACTIVE |
| 34 | +- `configuration`: JSON configuration for tenant-specific settings |
| 35 | + |
| 36 | +### 2. Tenant Context |
| 37 | + |
| 38 | +**Location**: `TenantContext.java` |
| 39 | + |
| 40 | +Thread-local storage for current tenant: |
| 41 | +- `setTenantId(String)`: Set current tenant ID |
| 42 | +- `getTenantId()`: Get current tenant ID |
| 43 | +- `setTenantCode(String)`: Set current tenant code |
| 44 | +- `getTenantCode()`: Get current tenant code |
| 45 | +- `clear()`: Clear tenant context |
| 46 | + |
| 47 | +### 3. Tenant Resolver |
| 48 | + |
| 49 | +**Location**: `TenantResolver.java` |
| 50 | + |
| 51 | +HTTP interceptor that extracts tenant information from: |
| 52 | +1. **X-Tenant-Id Header**: Direct tenant ID |
| 53 | +2. **X-Tenant-Code Header**: Tenant code (resolved to ID) |
| 54 | +3. **Subdomain**: Extracts tenant from subdomain (e.g., `tenant1.example.com`) |
| 55 | +4. **JWT Claims**: (Future) Extract from JWT token |
| 56 | + |
| 57 | +### 4. Tenant-Aware Interface |
| 58 | + |
| 59 | +**Location**: `TenantAware.java` |
| 60 | + |
| 61 | +Marker interface for entities that support multi-tenancy: |
| 62 | +```java |
| 63 | +public interface TenantAware { |
| 64 | + String getTenantId(); |
| 65 | + void setTenantId(String tenantId); |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +### 5. Hibernate Tenant Filter |
| 70 | + |
| 71 | +**Location**: `TenantFilter.java` |
| 72 | + |
| 73 | +Automatic row-level filtering using Hibernate filters: |
| 74 | +- Filters data by `tenant_id` automatically |
| 75 | +- Applied to all queries for tenant-aware entities |
| 76 | + |
| 77 | +## Database Schema |
| 78 | + |
| 79 | +### Tenants Table |
| 80 | + |
| 81 | +```sql |
| 82 | +CREATE TABLE tenants ( |
| 83 | + id BIGINT AUTO_INCREMENT PRIMARY KEY, |
| 84 | + code VARCHAR(50) NOT NULL UNIQUE, |
| 85 | + name VARCHAR(200) NOT NULL, |
| 86 | + email VARCHAR(255), |
| 87 | + phone VARCHAR(20), |
| 88 | + status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', |
| 89 | + configuration TEXT, |
| 90 | + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 91 | + updated_at TIMESTAMP, |
| 92 | + created_by VARCHAR(255), |
| 93 | + updated_by VARCHAR(255) |
| 94 | +); |
| 95 | +``` |
| 96 | + |
| 97 | +### Tenant ID Column |
| 98 | + |
| 99 | +All tenant-aware entities have a `tenant_id` column: |
| 100 | +- `bookings.tenant_id` |
| 101 | +- `passengers.tenant_id` |
| 102 | +- (Add to other entities as needed) |
| 103 | + |
| 104 | +## Usage |
| 105 | + |
| 106 | +### 1. Creating a Tenant |
| 107 | + |
| 108 | +```http |
| 109 | +POST /api/tenants |
| 110 | +Content-Type: application/json |
| 111 | +
|
| 112 | +{ |
| 113 | + "code": "acme-corp", |
| 114 | + "name": "ACME Corporation", |
| 115 | + "email": "admin@acme.com", |
| 116 | + "phone": "+1234567890", |
| 117 | + "status": "ACTIVE" |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +### 2. Making Requests with Tenant Context |
| 122 | + |
| 123 | +**Option 1: Using X-Tenant-Id Header** |
| 124 | +```http |
| 125 | +GET /api/bookings |
| 126 | +X-Tenant-Id: 1 |
| 127 | +``` |
| 128 | + |
| 129 | +**Option 2: Using X-Tenant-Code Header** |
| 130 | +```http |
| 131 | +GET /api/bookings |
| 132 | +X-Tenant-Code: acme-corp |
| 133 | +``` |
| 134 | + |
| 135 | +**Option 3: Using Subdomain** |
| 136 | +```http |
| 137 | +GET http://acme-corp.example.com/api/bookings |
| 138 | +``` |
| 139 | + |
| 140 | +### 3. Creating a Booking (Tenant-Aware) |
| 141 | + |
| 142 | +```http |
| 143 | +POST /api/bookings |
| 144 | +X-Tenant-Id: 1 |
| 145 | +Content-Type: application/json |
| 146 | +
|
| 147 | +{ |
| 148 | + "userId": 1, |
| 149 | + "trainId": 1, |
| 150 | + "totalFare": 500.00, |
| 151 | + "passengers": [...] |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +The booking will automatically have `tenant_id` set to `1`. |
| 156 | + |
| 157 | +### 4. Querying Bookings |
| 158 | + |
| 159 | +```http |
| 160 | +GET /api/bookings |
| 161 | +X-Tenant-Id: 1 |
| 162 | +``` |
| 163 | + |
| 164 | +Only bookings with `tenant_id = 1` will be returned. |
| 165 | + |
| 166 | +## API Endpoints |
| 167 | + |
| 168 | +### Tenant Management |
| 169 | + |
| 170 | +- `POST /api/tenants` - Create tenant |
| 171 | +- `GET /api/tenants` - Get all tenants |
| 172 | +- `GET /api/tenants/{id}` - Get tenant by ID |
| 173 | +- `GET /api/tenants/code/{code}` - Get tenant by code |
| 174 | +- `PUT /api/tenants/{id}` - Update tenant |
| 175 | +- `DELETE /api/tenants/{id}` - Delete tenant |
| 176 | +- `POST /api/tenants/{id}/activate` - Activate tenant |
| 177 | +- `POST /api/tenants/{id}/suspend` - Suspend tenant |
| 178 | + |
| 179 | +## Security |
| 180 | + |
| 181 | +### Tenant Isolation |
| 182 | + |
| 183 | +1. **Automatic Filtering**: Hibernate filters automatically filter queries |
| 184 | +2. **Service-Level Validation**: Services validate tenant access |
| 185 | +3. **Context Validation**: TenantResolver validates tenant exists and is active |
| 186 | + |
| 187 | +### Access Control |
| 188 | + |
| 189 | +- Users can only access data belonging to their tenant |
| 190 | +- Cross-tenant access attempts are logged and blocked |
| 191 | +- Tenant context is required for all protected endpoints |
| 192 | + |
| 193 | +## Configuration |
| 194 | + |
| 195 | +### Application Properties |
| 196 | + |
| 197 | +```yaml |
| 198 | +multi-tenancy: |
| 199 | + enabled: true # Enable multi-tenancy support |
| 200 | + required: true # Require tenant context for all requests |
| 201 | + header-tenant-id: "X-Tenant-Id" |
| 202 | + header-tenant-code: "X-Tenant-Code" |
| 203 | + subdomain-extraction: true |
| 204 | +``` |
| 205 | +
|
| 206 | +## Implementation Details |
| 207 | +
|
| 208 | +### Entity Updates |
| 209 | +
|
| 210 | +All tenant-aware entities: |
| 211 | +1. Implement `TenantAware` interface |
| 212 | +2. Add `tenant_id` column |
| 213 | +3. Add Hibernate filter annotations |
| 214 | +4. Add index on `tenant_id` |
| 215 | + |
| 216 | +### Service Updates |
| 217 | + |
| 218 | +Services: |
| 219 | +1. Set `tenant_id` from context when creating entities |
| 220 | +2. Validate tenant access when reading entities |
| 221 | +3. Filter queries by tenant |
| 222 | + |
| 223 | +### Repository Updates |
| 224 | + |
| 225 | +Repositories: |
| 226 | +1. Use Hibernate filters for automatic filtering |
| 227 | +2. Add tenant-specific query methods if needed |
| 228 | + |
| 229 | +## Migration Strategy |
| 230 | + |
| 231 | +### Existing Data |
| 232 | + |
| 233 | +For existing data without `tenant_id`: |
| 234 | +1. Create a default tenant |
| 235 | +2. Update existing records with default tenant ID |
| 236 | +3. Or mark records as "legacy" and handle separately |
| 237 | + |
| 238 | +### Migration Script |
| 239 | + |
| 240 | +```sql |
| 241 | +-- Add tenant_id column |
| 242 | +ALTER TABLE bookings ADD COLUMN tenant_id VARCHAR(50); |
| 243 | +
|
| 244 | +-- Create default tenant |
| 245 | +INSERT INTO tenants (code, name, status) VALUES ('default', 'Default Tenant', 'ACTIVE'); |
| 246 | +
|
| 247 | +-- Update existing records (if needed) |
| 248 | +UPDATE bookings SET tenant_id = (SELECT id FROM tenants WHERE code = 'default'); |
| 249 | +``` |
| 250 | + |
| 251 | +## Testing |
| 252 | + |
| 253 | +### Unit Tests |
| 254 | + |
| 255 | +```java |
| 256 | +@Test |
| 257 | +public void testTenantIsolation() { |
| 258 | + // Set tenant context |
| 259 | + TenantContext.setTenantId("1"); |
| 260 | + |
| 261 | + // Create booking |
| 262 | + SimpleBooking booking = new SimpleBooking(); |
| 263 | + bookingService.createBooking(booking); |
| 264 | + |
| 265 | + // Verify tenant_id is set |
| 266 | + assertEquals("1", booking.getTenantId()); |
| 267 | + |
| 268 | + // Clear context |
| 269 | + TenantContext.clear(); |
| 270 | +} |
| 271 | +``` |
| 272 | + |
| 273 | +### Integration Tests |
| 274 | + |
| 275 | +```java |
| 276 | +@Test |
| 277 | +public void testTenantAccessControl() { |
| 278 | + // Create booking for tenant 1 |
| 279 | + TenantContext.setTenantId("1"); |
| 280 | + SimpleBooking booking = bookingService.createBooking(new SimpleBooking()); |
| 281 | + |
| 282 | + // Try to access as tenant 2 |
| 283 | + TenantContext.setTenantId("2"); |
| 284 | + Optional<SimpleBooking> result = bookingService.getBookingById(booking.getId()); |
| 285 | + |
| 286 | + // Should return empty |
| 287 | + assertFalse(result.isPresent()); |
| 288 | +} |
| 289 | +``` |
| 290 | + |
| 291 | +## Best Practices |
| 292 | + |
| 293 | +### 1. Always Set Tenant Context |
| 294 | + |
| 295 | +- Set tenant context early in request processing |
| 296 | +- Validate tenant exists and is active |
| 297 | +- Clear context after request completion |
| 298 | + |
| 299 | +### 2. Validate Tenant Access |
| 300 | + |
| 301 | +- Always validate tenant access in services |
| 302 | +- Log access violations |
| 303 | +- Return appropriate error messages |
| 304 | + |
| 305 | +### 3. Index Tenant ID |
| 306 | + |
| 307 | +- Add indexes on `tenant_id` columns |
| 308 | +- Improves query performance |
| 309 | +- Essential for large datasets |
| 310 | + |
| 311 | +### 4. Tenant Configuration |
| 312 | + |
| 313 | +- Use `configuration` JSON field for tenant-specific settings |
| 314 | +- Support feature flags per tenant |
| 315 | +- Customize behavior per tenant |
| 316 | + |
| 317 | +### 5. Monitoring |
| 318 | + |
| 319 | +- Monitor tenant usage |
| 320 | +- Track cross-tenant access attempts |
| 321 | +- Alert on suspicious activity |
| 322 | + |
| 323 | +## Troubleshooting |
| 324 | + |
| 325 | +### Issue: Tenant Context Not Set |
| 326 | + |
| 327 | +**Symptoms**: Requests fail with "Tenant context is required" |
| 328 | + |
| 329 | +**Solution**: |
| 330 | +- Ensure `X-Tenant-Id` or `X-Tenant-Code` header is sent |
| 331 | +- Check TenantResolver is registered |
| 332 | +- Verify interceptor is not excluded |
| 333 | + |
| 334 | +### Issue: Cross-Tenant Data Access |
| 335 | + |
| 336 | +**Symptoms**: User can see data from other tenants |
| 337 | + |
| 338 | +**Solution**: |
| 339 | +- Verify Hibernate filter is enabled |
| 340 | +- Check service-level validation |
| 341 | +- Review tenant context propagation |
| 342 | + |
| 343 | +### Issue: Performance Issues |
| 344 | + |
| 345 | +**Symptoms**: Slow queries with tenant filtering |
| 346 | + |
| 347 | +**Solution**: |
| 348 | +- Ensure indexes on `tenant_id` |
| 349 | +- Review query plans |
| 350 | +- Consider tenant-specific caching |
| 351 | + |
| 352 | +## Future Enhancements |
| 353 | + |
| 354 | +1. **JWT Integration**: Extract tenant from JWT claims |
| 355 | +2. **Tenant-Specific Databases**: Support separate databases per tenant |
| 356 | +3. **Tenant Analytics**: Per-tenant usage analytics |
| 357 | +4. **Tenant Billing**: Track usage per tenant |
| 358 | +5. **Tenant Onboarding**: Automated tenant provisioning |
| 359 | + |
| 360 | +## Files Created |
| 361 | + |
| 362 | +### Core Components |
| 363 | +- `Tenant.java` - Tenant entity |
| 364 | +- `TenantRepository.java` - Tenant repository |
| 365 | +- `TenantContext.java` - Thread-local tenant context |
| 366 | +- `TenantResolver.java` - HTTP interceptor |
| 367 | +- `TenantAware.java` - Marker interface |
| 368 | +- `TenantService.java` - Tenant business logic |
| 369 | +- `TenantController.java` - Tenant REST API |
| 370 | +- `TenantConfig.java` - Configuration |
| 371 | +- `TenantFilter.java` - Hibernate filter |
| 372 | + |
| 373 | +### Database Migrations |
| 374 | +- `V7__Create_tenants_table.sql` - Create tenants table |
| 375 | +- `V8__Add_tenant_id_to_bookings.sql` - Add tenant_id columns |
| 376 | + |
| 377 | +### Documentation |
| 378 | +- `MULTI_TENANCY_IMPLEMENTATION.md` - This file |
| 379 | + |
| 380 | +## Conclusion |
| 381 | + |
| 382 | +Multi-tenancy support is now implemented in the booking service. The same pattern can be applied to other services (train, user, payment, notification) by: |
| 383 | + |
| 384 | +1. Adding tenant-aware interfaces to entities |
| 385 | +2. Adding `tenant_id` columns |
| 386 | +3. Updating services to use tenant context |
| 387 | +4. Adding tenant validation |
| 388 | + |
| 389 | +This provides a solid foundation for SaaS capabilities with proper tenant isolation and security. |
| 390 | + |
0 commit comments