Shared Patterns Across Control Plane Services
All 10 Control Plane services implement the same architectural patterns, enforced through the commons-java library. This consistency reduces cognitive overhead when switching between services and ensures uniform behavior in security, error handling, observability, and multi-tenancy.
2.3.F.1Security Filter Chain
Every request passes through an ordered filter chain before reaching any controller:
Request arrives
|
v
SecurityFilter (HIGHEST_PRECEDENCE + 10)
- Validate request headers for injection attacks
- Check X-Tenant-ID, X-User-ID, X-Request-ID format
- Reject path traversal, null bytes, oversized payloads
|
v
JwtAuthenticationFilter (HIGHEST_PRECEDENCE + 20)
- Extract Bearer token from Authorization header
- Validate JWT signature, expiry, issuer, type
- Check blacklist (Redis: blacklist:{jti})
- Set Spring Security context
|
v
TenantContextFilter (HIGHEST_PRECEDENCE + 30)
- Read X-Tenant-ID header (set by gateway)
- Call TenantContextHolder.setTenantId()
- ALWAYS clear context in finally block
|
v
RbacFilter (HIGHEST_PRECEDENCE + 40)
- Read @RequirePermission from target controller method
- Check user roles against required permissions
- Return 403 Forbidden if insufficient
|
v
Controller --> Service --> RepositoryThe SecurityFilter is critical for defense-in-depth. Even though the Kong gateway performs input validation, the backend SecurityFilter provides a second layer that catches any requests that bypass the gateway (e.g., direct pod-to-pod communication within the cluster).
2.3.F.2API Response Envelope
All Control Plane APIs return responses in a consistent envelope format:
Success Response
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "ACME Corporation",
"tier": "enterprise",
"status": "active"
},
"metadata": {
"timestamp": "2026-02-12T10:30:00Z",
"requestId": "req-abc-123",
"version": "v1"
}
}Error Response
{
"success": false,
"error": {
"code": "RESOURCE_NOT_FOUND",
"category": "CLIENT_ERROR",
"message": "Tenant with ID 'xyz' not found",
"details": {
"resource": "tenant",
"identifier": "xyz"
}
},
"metadata": {
"timestamp": "2026-02-12T10:30:00Z",
"requestId": "req-abc-123"
}
}Error Code Catalog
| Code | HTTP Status | Category | Description |
|---|---|---|---|
VALIDATION_FAILED | 400 | CLIENT_ERROR | Request body failed validation |
AUTHENTICATION_REQUIRED | 401 | AUTH_ERROR | Missing or invalid JWT token |
INSUFFICIENT_PERMISSIONS | 403 | AUTH_ERROR | User lacks required permissions |
RESOURCE_NOT_FOUND | 404 | CLIENT_ERROR | Requested resource does not exist |
RESOURCE_CONFLICT | 409 | CLIENT_ERROR | Resource already exists |
RATE_LIMITED | 429 | CLIENT_ERROR | Tenant exceeded rate limit |
INTERNAL_ERROR | 500 | SERVER_ERROR | Unhandled server error |
SERVICE_UNAVAILABLE | 503 | SERVER_ERROR | Downstream dependency unavailable |
2.3.F.3Structured Logging
All services use StructuredLoggingConfig from commons-java, which enriches every log line with context:
{
"timestamp": "2026-02-12T10:30:00.123Z",
"level": "INFO",
"logger": "com.matih.tenant.service.TenantService",
"service": "tenant-service",
"tenant_id": "acme-corp",
"user_id": "user-123",
"correlation_id": "cor-abc-456",
"request_id": "req-def-789",
"trace_id": "abc123def456",
"span_id": "789ghi",
"message": "Tenant provisioning started",
"context": {
"phase": "CREATE_NAMESPACE",
"tier": "enterprise"
}
}The tenant_id, user_id, and correlation_id fields are automatically injected from the TenantContextHolder and request headers via MDC (Mapped Diagnostic Context). This means every log line in the request's processing chain carries the same context, enabling precise filtering in Loki.
2.3.F.4Health Checks
Each service registers deep health checks via ComponentHealthCheck:
@Component
public class DatabaseHealthCheck implements ComponentHealthCheck {
@Override
public Health check() {
try {
jdbcTemplate.execute("SELECT 1");
return Health.up()
.withDetail("database", "connected")
.withDetail("responseTime", "2ms")
.build();
} catch (Exception e) {
return Health.down()
.withDetail("database", "unreachable")
.withException(e)
.build();
}
}
}Health checks go beyond simple liveness probes -- they verify that the service can perform its core functions:
| Check | Verifies |
|---|---|
DatabaseHealthCheck | PostgreSQL connection and query execution |
RedisHealthCheck | Redis connection and PING response |
KafkaHealthCheck | Kafka broker connectivity |
DiskSpaceHealthCheck | Sufficient disk space for logs and temp files |
MemoryHealthCheck | JVM heap usage below threshold |
Kubernetes probes are configured:
# From Helm chart templates
livenessProbe:
httpGet:
path: /api/v1/actuator/health/liveness
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/v1/actuator/health/readiness
port: 8081
initialDelaySeconds: 15
periodSeconds: 5
failureThreshold: 32.3.F.5Pagination and Filtering
All list endpoints support consistent pagination and filtering:
GET /api/v1/users?page=0&size=20&sort=createdAt,desc&status=ACTIVE&search=jane| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 0 | Zero-based page number |
size | int | 20 | Page size (max 100) |
sort | string | createdAt,desc | Sort field and direction |
search | string | none | Full-text search across indexed fields |
| Filter params | varies | none | Entity-specific filters (e.g., status, tier) |
Response includes pagination metadata:
{
"success": true,
"data": [...],
"pagination": {
"page": 0,
"size": 20,
"totalElements": 156,
"totalPages": 8,
"hasNext": true,
"hasPrevious": false
}
}2.3.F.6Circuit Breaker Pattern
Inter-service calls use circuit breakers from commons-java:
CLOSED (normal operation)
|
+--> 5 consecutive failures --> OPEN (fail fast, return 503)
| |
| +--> after 30s --> HALF_OPEN
| |
| 3 successful requests -->|
| |
+<----------- Reset to CLOSED <---------------------+Circuit breaker state is local to each pod (not shared via Redis). This means different replicas may have different circuit breaker states, which is intentional -- it allows partial recovery where healthy replicas can reach the downstream service even if one replica's circuit is open.
2.3.F.7Database Access Patterns
Hibernate Multi-Tenancy
All repository classes automatically scope queries to the current tenant:
// TenantIdentifierResolver routes Hibernate to the correct schema
@Component
public class TenantIdentifierResolver
implements CurrentTenantIdentifierResolver<String> {
@Override
public String resolveCurrentTenantIdentifier() {
return TenantContext.getCurrentTenantIdOrDefault("system");
}
}
// Before each query, Hibernate executes:
// SET search_path TO '{tenant_schema}';
// Then the actual query runs against the correct schemaOptimistic Locking
Entities use @Version for optimistic concurrency control:
@Entity
public class Dashboard {
@Id
private UUID id;
@Version
private Long version; // Auto-incremented on each update
// If two concurrent updates read version=5,
// the first update succeeds (sets version=6),
// the second gets OptimisticLockException
}Related Sections
- Control Plane Overview -- All service descriptions
- API Design -- REST conventions and error handling
- Multi-Tenancy -- TenantContext propagation details