A robust validation framework is essential for maintaining data integrity and security in backend systems, requiring careful design considerations for extensibility, performance, and maintainability across diverse validation requirements.
1. Problem Statement & Requirements
Clarifying questions
- What is the scope of validation? Should this framework support validation for API inputs, database entities, configuration files, or all of the above?
- What is the expected scale? Are we targeting microservices with high throughput (e.g., 10k+ requests/sec) or smaller-scale applications?
- What level of consistency is required? Do we need strong consistency across validation rules, or can some validation be eventually consistent?
- What's the read/write ratio? Is this primarily used for validating incoming data (high write) or also for querying validation results?
- What are the latency requirements? Are we targeting sub-10ms validation for real-time API use, or can we tolerate higher latencies?
- What authentication model is in place? Do we need to support JWT, OAuth2, or API keys for validating access to validation rules?
- What external systems integrate with this? Do we need to support integration with external validation services or rule repositories?
Functional requirements
- Validate input data against defined rules (e.g., field presence, format, range)
- Support custom validation rules with extensible rule engine
- Validate complex nested objects and collections
- Return detailed error messages with field paths for failed validations
- Support validation of data from various sources (API requests, database entities, config files)
- Allow rule versioning and rollback capability
- Support conditional validation (e.g., validate field X only if field Y has value Z)
- Provide audit trail of validation attempts and results
Non-functional requirements
- Latency targets: p50 < 5ms, p95 < 10ms, p99 < 25ms for validation operations
- Throughput: 10k+ validations/sec under load (with 50% concurrent validation requests)
- Availability: 99.9% uptime with graceful degradation capability
- Scalability: Support horizontal scaling across microservices and distributed environments
- Extensibility: Allow runtime rule addition without service restarts
Constraints and assumptions
- Assumption: Validation rules will be defined in a centralized rule repository or per-service configuration
- Constraint: Framework must support Java 11+ and be compatible with Spring Boot ecosystem
- Assumption: Validation will primarily occur in request/response lifecycle (API layer)
- Constraint: Must support both synchronous and asynchronous validation modes
- Assumption: Validation results will be persisted for audit purposes (optional)
- Constraint: Framework should not introduce significant performance overhead to the core business logic
Out of scope
- Rule engine implementation (external rule engines like Drools or Camunda are out of scope)
- UI for managing validation rules (admin dashboard is out of scope)
- Real-time rule updates via webhooks or streaming
- Validation of data in transit (e.g., encryption/decryption validation)
- Cross-service validation orchestration (e.g., validating data across multiple microservices)
- Machine learning-based validation (anomaly detection, etc.)
- Validation of external system states or dependencies (e.g., database connectivity checks)
2. API Design
API Style Selection
For a validation framework, I'll select gRPC as the primary API style for the following reasons:
- Strong typing: gRPC's Protocol Buffers provide compile-time type safety, which is crucial for a validation framework where correctness is paramount
- Performance: Binary serialization is more efficient than JSON for validation operations, especially when validating large payloads
- Streaming support: gRPC supports bidirectional streaming which can be useful for real-time validation feedback
- Service contracts: gRPC provides clear service definitions that make it easy to version and evolve the API
- Built-in error handling: gRPC's standardized error codes and status messages align well with validation use cases
Endpoint Definitions
| Method | Path | Description | Auth |
|---|---|---|---|
| ValidateRequest | /validation.v1.ValidationService/ValidateRequest | Validates a single request against configured rules | JWT |
| ValidateBatch | /validation.v1.ValidationService/ValidateBatch | Validates multiple requests in batch mode | JWT |
| GetValidationRules | /validation.v1.ValidationService/GetValidationRules | Retrieves validation rules for a specific entity type | JWT |
| CreateValidationRule | /validation.v1.ValidationService/CreateValidationRule | Creates a new validation rule | JWT |
| UpdateValidationRule | /validation.v1.ValidationService/UpdateValidationRule | Updates an existing validation rule | JWT |
| DeleteValidationRule | /validation.v1.ValidationService/DeleteValidationRule | Deletes a validation rule | JWT |
Request and Response Schemas
ValidateRequest Request:
message ValidateRequest {
string entity_type = 1;
google.protobuf.Struct payload = 2;
repeated ValidationContext contexts = 3;
bool return_all_errors = 4;
}
message ValidationContext {
string context_key = 1;
string context_value = 2;
}
message ValidateRequestResponse {
bool is_valid = 1;
repeated ValidationError errors = 2;
google.protobuf.Struct validated_payload = 3;
}
ValidateBatch Request:
message ValidateBatchRequest {
repeated ValidateRequest requests = 1;
bool return_all_errors = 2;
}
message ValidateBatchResponse {
repeated ValidateRequestResponse responses = 1;
}
Validation Rule Schema:
message ValidationRule {
string id = 1;
string entity_type = 2;
string field_path = 3;
ValidationRuleType rule_type = 4;
google.protobuf.Value config = 5;
string description = 6;
bool is_active = 7;
int32 priority = 8;
repeated string tags = 9;
google.protobuf.Timestamp created_at = 10;
google.protobuf.Timestamp updated_at = 11;
}
enum ValidationRuleType {
REQUIRED = 0;
LENGTH = 1;
REGEX = 2;
RANGE = 3;
CUSTOM = 4;
}
HTTP Status Codes and Error Response Format
Standard gRPC error codes will be used:
INVALID_ARGUMENT(400): Invalid input parameters or malformed validation requestsUNAUTHENTICATED(401): Missing or invalid authentication tokenPERMISSION_DENIED(403): Insufficient permissions to access validation rulesNOT_FOUND(404): Validation rule not found or entity type not supportedINTERNAL(500): Internal server errors during validation processing
Standardized error response format:
message ValidationError {
string field_path = 1;
string error_code = 2;
string message = 3;
repeated string details = 4;
google.protobuf.Struct context = 5;
}
API Versioning Strategy
Versioned using semantic versioning in the service name:
validation.v1.ValidationService- Initial stable version- Future:
validation.v2.ValidationServicefor breaking changes
Versioning approach:
- Backward compatible: New fields added to messages with default values
- Breaking changes: Major version bump (v2, v3)
- Minor versions: Non-breaking enhancements within same major version
Pagination Design
For fetching validation rules, we'll use cursor-based pagination:
message ListValidationRulesRequest {
string entity_type = 1;
string cursor = 2;
int32 page_size = 3;
string sort_by = 4;
SortOrder order = 5;
}
message ListValidationRulesResponse {
repeated ValidationRule rules = 1;
string next_cursor = 2;
int64 total_count = 3;
}
enum SortOrder {
ASC = 0;
DESC = 1;
}
Justification: Cursor-based pagination provides better consistency and handles data changes during pagination better than offset-based approaches, which is critical for a validation system where rule consistency matters.
Authentication and Authorization
Authentication: JWT Bearer tokens with standard OAuth2 flow for service-to-service communication.
Authorization:
- All endpoints require
validation.readorvalidation.writescope - Rule creation/deletion requires
validation.adminscope - Entity-specific validation requires appropriate entity permissions
Rate Limiting Strategy
Rate limiting per endpoint:
| Endpoint | Limit | Window | Enforcement |
|---|---|---|---|
| ValidateRequest | 1000 req/min | 1 minute | In-memory + Redis |
| ValidateBatch | 100 req/min | 1 minute | In-memory + Redis |
| GetValidationRules | 500 req/min | 1 minute | In-memory + Redis |
| Create/Update/Delete Rules | 50 req/min | 1 minute | In-memory + Redis |
Enforcement Strategy:
- Token bucket algorithm for each endpoint
- Distributed rate limiting using Redis with atomic operations
- Per-tenant rate limiting for multi-tenant systems
Idempotency Design
For mutation endpoints, we'll implement idempotency key strategy:
message IdempotencyRequest {
string idempotency_key = 1;
google.protobuf.Any request = 2;
}
message IdempotencyResponse {
string idempotency_key = 1;
google.protobuf.Any response = 2;
}
Implementation approach:
- Store idempotency keys in Redis with TTL of 5 minutes
- For CreateValidationRule, check if key exists and return same response
- Use consistent hashing for key distribution across Redis instances
3. Data Model Design
Entity-Relationship diagram (Mermaid)
erDiagram
VALIDATION_RULE {
string id PK
string name
string description
string rule_type
string expression
string validation_group
boolean is_active
timestamp created_at
timestamp updated_at
string created_by
string updated_by
}
VALIDATION_GROUP {
string id PK
string name
string description
timestamp created_at
timestamp updated_at
}
VALIDATION_RESULT {
string id PK
string entity_id
string validation_rule_id FK
string status
string message
timestamp created_at
timestamp updated_at
}
VALIDATION_CONTEXT {
string id PK
string entity_type
string entity_id
string validation_group_id FK
timestamp created_at
timestamp updated_at
}
VALIDATION_RULE ||--o{ VALIDATION_RESULT : "has"
VALIDATION_GROUP ||--o{ VALIDATION_RULE : "contains"
VALIDATION_CONTEXT ||--o{ VALIDATION_RESULT : "results_in"
VALIDATION_CONTEXT ||--o{ VALIDATION_RULE : "applies_to"
Table / Collection schema definitions with field types, constraints, indices
-- ValidationRule table
CREATE TABLE validation_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
rule_type VARCHAR(50) NOT NULL, -- 'REGEX', 'CUSTOM', 'BUILT_IN'
expression TEXT NOT NULL,
validation_group_id UUID REFERENCES validation_groups(id),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(255),
updated_by VARCHAR(255),
CONSTRAINT chk_rule_type CHECK (rule_type IN ('REGEX', 'CUSTOM', 'BUILT_IN')),
CONSTRAINT chk_name_not_empty CHECK (LENGTH(TRIM(name)) > 0)
);
-- ValidationGroup table
CREATE TABLE validation_groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- ValidationResult table
CREATE TABLE validation_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_id VARCHAR(255) NOT NULL,
entity_type VARCHAR(100) NOT NULL,
validation_rule_id UUID REFERENCES validation_rules(id),
status VARCHAR(20) NOT NULL, -- 'PASSED', 'FAILED', 'SKIPPED'
message TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_status CHECK (status IN ('PASSED', 'FAILED', 'SKIPPED'))
);
-- ValidationContext table
CREATE TABLE validation_contexts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type VARCHAR(100) NOT NULL,
entity_id VARCHAR(255) NOT NULL,
validation_group_id UUID REFERENCES validation_groups(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Indexes
CREATE INDEX idx_validation_rules_group_id ON validation_rules(validation_group_id);
CREATE INDEX idx_validation_rules_active ON validation_rules(is_active);
CREATE INDEX idx_validation_results_entity_type_id ON validation_results(entity_type, entity_id);
CREATE INDEX idx_validation_results_rule_id ON validation_results(validation_rule_id);
CREATE INDEX idx_validation_contexts_entity_type_id ON validation_contexts(entity_type, entity_id);
CREATE INDEX idx_validation_contexts_group_id ON validation_contexts(validation_group_id);
Primary key strategy (UUID vs. auto-increment vs. ULID — with justification)
Using UUID as primary key for all tables because:
- Distributed system compatibility: UUIDs provide globally unique identifiers without requiring coordination between services
- Database sharding: UUIDs distribute data evenly across sharded databases without requiring sequential IDs
- No single point of failure: Eliminates the need for centralized ID generation services
- Cross-service consistency: UUIDs can be generated client-side, enabling better integration with microservices
Index design (composite, partial, covering indices — with justification per index)
| Index Name | Columns | Type | Purpose |
|---|---|---|---|
| idx_validation_rules_group_id | validation_group_id | Standard | Fast lookup of rules by group |
| idx_validation_rules_active | is_active | Standard | Efficient filtering of active rules |
| idx_validation_results_entity_type_id | entity_type, entity_id | Composite | Fast validation result lookup by entity |
| idx_validation_results_rule_id | validation_rule_id | Standard | Fast rule-based result filtering |
| idx_validation_contexts_entity_type_id | entity_type, entity_id | Composite | Fast context lookup by entity |
| idx_validation_contexts_group_id | validation_group_id | Standard | Fast group-based context lookup |
Data partitioning / sharding strategy
Partition by entity_type for validation contexts and results:
- Each entity type gets its own partition (e.g., "user", "order", "product")
- This enables efficient querying by entity type and supports horizontal scaling
- Partition key can be hashed to distribute data across shards
Soft delete vs. hard delete decision with justification
Soft delete is used for validation rules and groups to:
- Audit trail: Maintain historical context of rule changes
- Recovery capability: Ability to restore deleted rules if needed
- Referential integrity: Prevent breaking references in validation results
- Compliance: Support for regulatory requirements that may require data retention
Audit trail design (who, what, when)
All entities have:
created_byandupdated_byfields storing user/actor identifiercreated_atandupdated_attimestamps with timezone support- Audit logs maintained in a separate audit table for compliance
Data archival strategy and retention policy
-
Validation results:
- Retention: 30 days for active validation results
- Archival: Move to cold storage (e.g., S3) after 30 days
- Purge: Delete completely after 1 year
-
Validation rules:
- Retention: Indefinite (soft deleted records retained)
- Archival: Historical snapshots stored in separate archive tables
-
Validation contexts:
- Retention: 90 days (based on typical business process duration)
- Archival: Move to archive tables after 90 days
// Example of soft-delete pattern in Java
@Entity
@Table(name = "validation_rules")
public class ValidationRule {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid2")
private UUID id;
@Column(nullable = false)
private String name;
@Column
private String description;
@Column(nullable = false)
private String ruleType;
@Column(nullable = false, columnDefinition = "TEXT")
private String expression;
@Column
private boolean isActive = true;
@Column
private LocalDateTime createdAt;
@Column
private LocalDateTime updatedAt;
@Column
private String createdBy;
@Column
private String updatedBy;
// Soft delete marker - set to true when deleted
@Column(name = "is_deleted", nullable = false, defaultValue = "false")
private boolean isDeleted = false;
// ... getters and setters
}
4. Class Design & Service Layer
UML Class Diagram
classDiagram
class ValidationFramework {
+validate(Object target, Class~T~ type)
+registerValidator(Validator validator)
}
class Validator {
<<Interface>>
+validate(Object target, ValidationContext context)
}
class ValidationContext {
+getValidationRules()
+getErrors()
+addError(ValidationError error)
}
class ValidationError {
+getMessage()
+getField()
+getErrorCode()
}
class ValidationRule {
+getName()
+getDescription()
+getPriority()
}
class FieldValidator {
+validate(Object target, ValidationContext context)
+getRule()
}
class Constraint {
<<Interface>>
+validate(Object value, ValidationContext context)
}
class ConstraintValidator {
<<Interface>>
+isValid(Object value, ValidationContext context)
+getMessage()
}
class DefaultConstraintValidator {
+isValid(Object value, ValidationContext context)
+getMessage()
}
class ValidationEngine {
+executeValidations(Object target, Class~T~ type)
+buildValidationChain()
}
class ValidationPipeline {
+process(Object target, ValidationContext context)
+addValidator(Validator validator)
}
class AnnotationBasedValidator {
+validate(Object target, ValidationContext context)
+extractRulesFromAnnotations(Class~T~ type)
}
class ValidationException {
+getErrors()
+getMessage()
}
ValidationFramework --> ValidationEngine
ValidationFramework --> Validator : uses
ValidationEngine --> ValidationPipeline
ValidationPipeline --> Validator
Validator <|-- FieldValidator
Validator <|-- AnnotationBasedValidator
ValidationContext --> ValidationError
ValidationRule --> Constraint
Constraint --> ConstraintValidator
ConstraintValidator <|-- DefaultConstraintValidator
ValidationEngine --> ValidationContext
AnnotationBasedValidator --> ValidationRule
ValidationException --> ValidationError
class FieldValidationRule {
+getFieldName()
+getConstraints()
}
FieldValidator --> FieldValidationRule
State Machine Diagram
stateDiagram-v2
[*] --> Idle
Idle --> Validating : validate()
Validating --> Success : all_valid
Validating --> Failed : validation_error
Validating --> Error : system_error
Success --> [*]
Failed --> [*]
Error --> [*]
Service Class Structure and Responsibilities
The validation framework consists of several key service classes:
- ValidationFramework - Main entry point for validation operations
- ValidationEngine - Core engine that orchestrates validation execution
- ValidationPipeline - Manages the sequence of validators to execute
- AnnotationBasedValidator - Handles annotation-driven validation rules
- FieldValidator - Processes field-specific validation logic
Interface / Abstract Class Design
// Validator interface - defines contract for all validators
public interface Validator<T> {
boolean validate(T target, ValidationContext context);
String getValidatorName();
}
// Constraint interface - defines validation rule contracts
public interface Constraint<T> {
boolean isValid(T value, ValidationContext context);
String getMessage();
}
// ValidationContext - holds validation state and results
public class ValidationContext {
private final List<ValidationError> errors = new ArrayList<>();
private final Map<String, Object> metadata = new HashMap<>();
public void addError(ValidationError error) {
errors.add(error);
}
public List<ValidationError> getErrors() {
return new ArrayList<>(errors);
}
public boolean hasErrors() {
return !errors.isEmpty();
}
}
// ValidationError - represents a single validation failure
public class ValidationError {
private final String field;
private final String message;
private final String errorCode;
public ValidationError(String field, String message, String errorCode) {
this.field = field;
this.message = message;
this.errorCode = errorCode;
}
// Getters...
}
// ValidationException - thrown when validation fails
public class ValidationException extends Exception {
private final List<ValidationError> errors;
public ValidationException(List<ValidationError> errors) {
super("Validation failed with " + errors.size() + " errors");
this.errors = errors;
}
public List<ValidationError> getErrors() {
return errors;
}
}
Business Logic Encapsulation Patterns
The framework uses the Strategy Pattern for validation rule implementation:
// Strategy pattern implementation for constraint validation
public class ValidationStrategy {
private final Map<String, ConstraintValidator> validators = new HashMap<>();
public void registerValidator(String constraintType, ConstraintValidator validator) {
validators.put(constraintType, validator);
}
public boolean validate(Object value, String constraintType, ValidationContext context) {
ConstraintValidator validator = validators.get(constraintType);
if (validator == null) {
throw new IllegalArgumentException("No validator found for constraint: " + constraintType);
}
return validator.isValid(value, context);
}
}
// Concrete implementation of a constraint validator
public class NotNullValidator implements ConstraintValidator<String> {
@Override
public boolean isValid(String value, ValidationContext context) {
return value != null;
}
@Override
public String getMessage() {
return "Field cannot be null";
}
}
Critical-Path Method Implementation
public class ValidationFramework {
private final ValidationEngine engine = new ValidationEngine();
private final List<Validator<?>> validators = new ArrayList<>();
public <T> ValidationResult validate(T target, Class<T> type) throws ValidationException {
// Transaction boundary - single validation operation
ValidationContext context = new ValidationContext();
try {
// Execute all registered validators in pipeline
boolean isValid = engine.executeValidations(target, type, context);
if (!isValid) {
throw new ValidationException(context.getErrors());
}
return new ValidationResult(true, context.getErrors());
} catch (Exception e) {
// Log and rethrow validation errors
logger.error("Validation failed for target type: {}", type.getSimpleName(), e);
throw new ValidationException(context.getErrors());
}
}
public void registerValidator(Validator<?> validator) {
validators.add(validator);
}
}
// Core validation engine implementation
public class ValidationEngine {
private final ValidationPipeline pipeline = new ValidationPipeline();
public <T> boolean executeValidations(T target, Class<T> type, ValidationContext context) {
// Build validation pipeline from registered validators
buildValidationChain(type);
// Execute each validator in sequence
for (Validator<T> validator : pipeline.getValidators()) {
if (!validator.validate(target, context)) {
// Early termination on validation failure
return false;
}
}
return true;
}
private <T> void buildValidationChain(Class<T> type) {
// This would typically inspect annotations or configuration
// and build the appropriate validator chain
pipeline.clear();
// Add validators based on type configuration...
}
}
// Field-level validator implementation
public class FieldValidator implements Validator<Object> {
private final String fieldName;
private final List<ConstraintValidator<?>> constraints;
public FieldValidator(String fieldName, List<ConstraintValidator<?>> constraints) {
this.fieldName = fieldName;
this.constraints = constraints;
}
@Override
public boolean validate(Object target, ValidationContext context) {
try {
// Get field value using reflection
Object fieldValue = getFieldValue(target, fieldName);
// Apply all constraints to the field value
for (ConstraintValidator<?> constraint : constraints) {
if (!constraint.isValid(fieldValue, context)) {
ValidationError error = new ValidationError(
fieldName,
constraint.getMessage(),
"VALIDATION_ERROR"
);
context.addError(error);
return false;
}
}
return true;
} catch (Exception e) {
ValidationError error = new ValidationError(
fieldName,
"Field validation failed: " + e.getMessage(),
"FIELD_VALIDATION_ERROR"
);
context.addError(error);
return false;
}
}
private Object getFieldValue(Object target, String fieldName) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(target);
}
@Override
public String getValidatorName() {
return "FieldValidator:" + fieldName;
}
}
// Result object for validation operations
public class ValidationResult {
private final boolean isValid;
private final List<ValidationError> errors;
public ValidationResult(boolean isValid, List<ValidationError> errors) {
this.isValid = isValid;
this.errors = errors;
}
public boolean isValid() {
return isValid;
}
public List<ValidationError> getErrors() {
return errors;
}
}
Transaction Boundary Design
The validation operation is treated as a single transaction boundary:
- Atomicity: All validations in the pipeline execute together or fail as a unit
- Isolation: Validation context is isolated per operation to prevent cross-contamination
- Consistency: All validation errors are collected and reported together
- Durability: Validation results are immediately available to caller
Service Interface Contracts
public interface ValidationService {
<T> ValidationResult validate(T target, Class<T> type) throws ValidationException;
void registerValidator(Validator<?> validator);
<T> ValidationResult validate(T target, Class<T> type, String... validationGroups);
}
Saga / Orchestration Pattern
For complex validation scenarios involving external systems:
public class ValidationSaga {
private final List<Validator> validators = new ArrayList<>();
public void executeValidationWithExternalDependencies(Object target) {
// Start saga - validate local constraints first
ValidationContext context = new ValidationContext();
// Execute local validations in pipeline
boolean localValid = executeLocalValidations(target, context);
if (localValid) {
// Execute external validations in parallel or sequential
executeExternalValidations(target, context);
}
// Final validation result
if (context.hasErrors()) {
throw new ValidationException(context.getErrors());
}
}
}
Domain Object / Value Object Design
// Validation rule value object
public class ValidationRule {
private final String fieldName;
private final List<Constraint> constraints;
private final int priority;
public ValidationRule(String fieldName, List<Constraint> constraints, int priority) {
this.fieldName = fieldName;
this.constraints = new ArrayList<>(constraints);
this.priority = priority;
}
// Getters...
}
// Validation configuration object
public class ValidationConfiguration {
private final Map<String, List<ValidationRule>> rulesByGroup = new HashMap<>();
private final Set<String> enabledGroups = new HashSet<>();
public void addRule(String group, ValidationRule rule) {
rulesByGroup.computeIfAbsent(group, k -> new ArrayList<>()).add(rule);
}
public List<ValidationRule> getRulesForGroup(String group) {
return rulesByGroup.getOrDefault(group, Collections.emptyList());
}
}
5. Concurrency & Thread Safety
Concurrency Model
The validation framework will operate in a multi-threaded environment where multiple validation requests can be processed concurrently. The system uses a thread pool model with a fixed-size executor service to manage concurrent validation tasks. Each validation request is processed in its own thread, ensuring that long-running validations don't block other concurrent operations.
Race Condition Prevention Strategy
Race conditions in the validation framework are mitigated through:
- Immutable validation configuration objects to prevent concurrent modification
- Thread-local storage for validation context and temporary state during validation runs
- Synchronized access to shared resources like validation caches and rule registries
- Atomic operations for counters and metrics tracking
Optimistic vs. Pessimistic Locking Selection
For the validation framework, optimistic locking is selected because:
- Validation operations are typically read-heavy with minimal write conflicts
- The framework processes independent validation requests concurrently without shared mutable state
- Lock contention would significantly impact throughput for concurrent validations
- Validation results are immutable once computed, making pessimistic locking unnecessary
Deadlock Prevention Strategy
Deadlock prevention is addressed through:
- Lock ordering: All validation components acquire locks in a consistent order (e.g., by rule ID or namespace)
- Timeout-based acquisition: Locks are acquired with a timeout to prevent indefinite blocking
- Hierarchical lock management: Validation rules and configuration are organized in a hierarchy to avoid circular dependencies
- Thread pool size limiting: Controlled concurrency prevents excessive lock contention
Idempotency Implementation for Concurrent Duplicate Requests
Duplicate validation requests are handled through:
public class ValidationRequestHandler {
private final Map<String, CompletableFuture<ValidationResult>> activeValidations =
new ConcurrentHashMap<>();
public CompletableFuture<ValidationResult> handleValidation(ValidationRequest request) {
String requestId = generateRequestId(request);
return activeValidations.computeIfAbsent(requestId, key -> {
CompletableFuture<ValidationResult> future = CompletableFuture.supplyAsync(() -> {
try {
// Check if validation is already complete
ValidationResult existing = getCompletedValidation(key);
if (existing != null) {
return existing;
}
// Perform actual validation
return performValidation(request);
} catch (Exception e) {
throw new ValidationException("Validation failed", e);
}
});
// Clean up after completion
future.thenRun(() -> activeValidations.remove(key));
return future;
});
}
private String generateRequestId(ValidationRequest request) {
// Create unique ID based on request parameters and timestamp
return UUID.randomUUID().toString();
}
}
The framework ensures that concurrent duplicate requests for the same validation input return the same result without re-computing, using a combination of:
- Request ID generation based on input parameters
- In-memory cache of active validation futures
- Cleanup of completed validations to prevent memory leaks
Thread Safety Patterns in Validation Components
The validation framework implements the following thread safety patterns:
-
Immutable Configuration Objects: Validation rules and configurations are immutable to prevent concurrent modification issues.
-
Thread-Local Context Management: Each validation thread maintains its own context using
ThreadLocalto avoid cross-contamination between concurrent validations. -
Synchronized Collections: For shared resources like rule registries and validation caches, synchronized collections or concurrent data structures are used.
-
Atomic Operations: For counters and metrics tracking, atomic operations ensure thread-safe updates without explicit locking.
Validation Rule Thread Safety
Validation rules are designed to be stateless and thread-safe:
public interface ValidationRule {
ValidationResult validate(Object input, ValidationContext context);
}
// Example thread-safe rule implementation
public class ThreadSafeRule implements ValidationRule {
private final String ruleId;
private final String description;
public ThreadSafeRule(String ruleId, String description) {
this.ruleId = ruleId;
this.description = description;
}
@Override
public ValidationResult validate(Object input, ValidationContext context) {
// Stateless validation logic - no shared mutable state
return validateInput(input, context);
}
private ValidationResult validateInput(Object input, ValidationContext context) {
// Implementation that does not modify any shared state
return new ValidationResult(true, "Valid");
}
}
Resource Pooling and Thread Safety
The framework uses thread-safe resource pools for:
- Validation rule instances (reused across validations)
- Thread pools for async validation processing
- Memory buffers for validation result serialization
All resource pools are implemented using concurrent collections and proper synchronization to handle concurrent access from multiple validation threads.
6. Sequence Diagrams for Key Flows
Happy Path Sequence Diagram
sequenceDiagram
participant Client
participant ValidationService
participant ValidatorRegistry
participant Validator
participant EventPublisher
Client->>ValidationService: Submit data for validation
ValidationService->>ValidatorRegistry: Get validators for data type
ValidatorRegistry-->>ValidationService: Return list of validators
ValidationService->>Validator: Execute validation
Validator-->>ValidationService: Return validation result
ValidationService->>EventPublisher: Publish validation event
EventPublisher-->>ValidationService: Acknowledge event publish
ValidationService-->>Client: Return validation success/failure
Error / Edge Case Sequence Diagram
sequenceDiagram
participant Client
participant ValidationService
participant ValidatorRegistry
participant Validator
participant EventPublisher
Client->>ValidationService: Submit invalid data
ValidationService->>ValidatorRegistry: Get validators for data type
ValidatorRegistry-->>ValidationService: Return list of validators
ValidationService->>Validator: Execute validation
Validator-->>ValidationService: Return validation error
ValidationService->>EventPublisher: Publish validation failure event
EventPublisher-->>ValidationService: Acknowledge event publish
ValidationService-->>Client: Return validation error response
Async Flow Sequence Diagram
sequenceDiagram
participant Client
participant ValidationService
participant EventPublisher
participant ValidationConsumer
participant ValidatorRegistry
Client->>ValidationService: Submit async validation request
ValidationService->>EventPublisher: Publish validation event to queue
EventPublisher-->>ValidationService: Acknowledge event publish
ValidationService-->>Client: Return async validation ID
EventPublisher->>ValidationConsumer: Consume validation event
ValidationConsumer->>ValidatorRegistry: Get validators for data type
ValidatorRegistry-->>ValidationConsumer: Return list of validators
ValidationConsumer->>Validator: Execute validation
Validator-->>ValidationConsumer: Return validation result
ValidationConsumer->>EventPublisher: Publish validation completion event
EventPublisher-->>ValidationConsumer: Acknowledge event publish
7. Caching Design
Cache Strategy Selection
For validation framework, we'll use cache-aside pattern as it provides the most flexibility and control over cache lifecycle. This approach allows us to implement custom validation logic, handle cache misses gracefully, and maintain consistency with the underlying data store.
Cache Key Design
Cache keys will follow a structured namespace pattern:
validation:{validatorType}:{entityId}:{version}
Example key structure:
String cacheKey = String.format("validation:%s:%s:%d",
validatorType, entityId, version);
TTL Strategy
| Data Type | TTL | Reason |
|---|---|---|
| Validation Results | 5 minutes | Frequent updates, need to balance freshness with performance |
| Validator Metadata | 1 hour | Configuration rarely changes |
| Validation Rules | 30 minutes | Rules may change but not frequently |
Cache Eviction Policy
We'll implement LRU (Least Recently Used) eviction policy with a maximum capacity of 10,000 entries. This ensures that the most frequently accessed validation results remain in cache while preventing memory bloat.
Cache Stampede Prevention
To prevent cache stampede, we'll implement probabilistic early expiry:
public class CacheStampedePrevention {
private static final double EARLY_EXPIRY_RATIO = 0.2;
public boolean shouldRefresh(String key) {
// Randomly trigger refresh 20% of the time
return Math.random() < EARLY_EXPIRY_RATIO;
}
}
Distributed Cache Architecture
We'll use Redis Cluster with:
- 3 master nodes for data distribution
- 3 replica nodes for high availability
- Automatic failover capability
- Redis Sentinel for monitoring and failover
Cache Invalidation Strategy
We'll use event-driven invalidation with the following approach:
- When validation rules change, publish
validation-rules-updatedevent - When entities are modified, publish
entity-updatedevent - Cache consumers listen to these events and invalidate relevant entries
@Component
public class ValidationCacheInvalidator {
@EventListener
public void handleValidationRulesUpdated(ValidationRulesUpdatedEvent event) {
String cacheKeyPattern = "validation:*:" + event.getValidatorType() + ":*";
redisTemplate.delete(redisTemplate.keys(cacheKeyPattern));
}
@EventListener
public void handleEntityUpdated(EntityUpdatedEvent event) {
String cacheKeyPattern = "validation:*:" + event.getEntityId() + ":*";
redisTemplate.delete(redisTemplate.keys(cacheKeyPattern));
}
}
Cache Hit/Miss Metrics
We'll track cache performance with:
validation.cache.hits- Counter for cache hitsvalidation.cache.misses- Counter for cache missesvalidation.cache.evictions- Counter for evicted entriesvalidation.cache.latency- Histogram for cache access times
Cache Warm-up Strategy
During system startup, we'll warm up the cache with frequently accessed validation results:
@Component
public class ValidationCacheWarmer {
@EventListener
public void warmUpCache(StartupEvent event) {
// Preload validation results for popular validators
List<String> popularValidators = getPopularValidators();
popularValidators.forEach(validator -> {
preloadValidatorCache(validator);
});
}
}
8. Repository / Data Access Layer
Repository Pattern Implementation
The repository pattern is implemented to abstract data access logic and provide a clean separation between domain objects and persistence concerns. Each entity has its own repository interface with standardized CRUD operations.
public interface UserRepository extends Repository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByStatus(UserStatus status);
Page<User> findAll(Pageable pageable);
}
public interface ValidationRuleRepository extends Repository<ValidationRule, Long> {
List<ValidationRule> findByEntityType(String entityType);
Optional<ValidationRule> findByRuleNameAndEntityType(String ruleName, String entityType);
}
ORM vs Raw Query Decisions
For the validation framework, we use a hybrid approach:
- ORM for complex domain objects: Hibernate/JPA is used for User, ValidationRule entities due to their complex relationships and need for lazy loading
- Raw queries for performance-critical operations: Direct SQL is used for validation result aggregation and batch operations to avoid ORM overhead
@Repository
public class ValidationRuleRepositoryImpl implements ValidationRuleRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<ValidationRule> findByEntityType(String entityType) {
String jpql = "SELECT r FROM ValidationRule r WHERE r.entityType = :entityType";
return entityManager.createQuery(jpql, ValidationRule.class)
.setParameter("entityType", entityType)
.getResultList();
}
@Override
public List<ValidationResult> findValidationResultsForEntity(String entityType, String entityId) {
// Raw query for performance optimization
String sql = """
SELECT vr.*, vres.validation_status, vres.error_message
FROM validation_rules vr
JOIN validation_results vres ON vr.id = vres.rule_id
WHERE vr.entity_type = :entityType AND vres.entity_id = :entityId
""";
return entityManager.createNativeQuery(sql, "ValidationResultWithRule")
.setParameter("entityType", entityType)
.setParameter("entityId", entityId)
.getResultList();
}
}
Read vs Write Separation
We implement CQRS (Command Query Responsibility Segregation) for the validation framework to optimize read and write operations:
// Write-side - used for creating/updating validation rules
@Repository
public class ValidationRuleWriteRepository {
@PersistenceContext
private EntityManager entityManager;
public void save(ValidationRule rule) {
entityManager.persist(rule);
}
public void update(ValidationRule rule) {
entityManager.merge(rule);
}
}
// Read-side - optimized for querying validation results
@Repository
public class ValidationRuleReadRepository {
@PersistenceContext
private EntityManager entityManager;
public List<ValidationResult> findRecentResults(String entityType, String entityId) {
// Optimized query for read-heavy operations
String jpql = """
SELECT new com.company.validation.dto.ValidationResultDto(
vr.ruleName, vr.description, vres.validationStatus, vres.errorMessage
)
FROM ValidationRule vr
JOIN ValidationResult vres ON vr.id = vres.ruleId
WHERE vr.entityType = :entityType AND vres.entityId = :entityId
ORDER BY vres.createdAt DESC
""";
return entityManager.createQuery(jpql, ValidationResultDto.class)
.setParameter("entityType", entityType)
.setParameter("entityId", entityId)
.setMaxResults(50)
.getResultList();
}
}
Connection Pool Configuration
Connection pool is configured with:
- Pool size: 20-50 connections based on concurrent load
- Timeout settings: 30s connection timeout, 60s validation timeout
- Validation query: SELECT 1 to ensure connections are alive
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 30000
validation-timeout: 60000
idle-timeout: 300000
max-lifetime: 1800000
N+1 Query Prevention Strategy
We implement eager loading and batch fetching to prevent N+1 queries:
@Entity
@Table(name = "validation_rules")
@NamedEntityGraph(
name = "ValidationRule.withResults",
attributeNodes = @NamedAttributeNode(value = "validationResults", fetchType = FetchType.EAGER)
)
public class ValidationRule {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "entity_type_id")
private EntityType entityType;
@OneToMany(mappedBy = "rule", fetch = FetchType.LAZY)
@BatchSize(size = 20) // Batch loading for performance
private List<ValidationResult> validationResults;
// ... other fields and methods
}
// In repository query with JOIN FETCH to prevent N+1
@Query("SELECT r FROM ValidationRule r JOIN FETCH r.validationResults WHERE r.entityType = :entityType")
List<ValidationRule> findByEntityTypeWithResults(@Param("entityType") String entityType);
Batch Operation Design
Batch operations are implemented for bulk validation rule creation and result updates:
@Repository
public class BatchValidationRepository {
@PersistenceContext
private EntityManager entityManager;
public void batchCreateRules(List<ValidationRule> rules) {
int batchSize = 100;
for (int i = 0; i < rules.size(); i += batchSize) {
int end = Math.min(i + batchSize, rules.size());
List<ValidationRule> batch = rules.subList(i, end);
for (ValidationRule rule : batch) {
entityManager.persist(rule);
}
// Flush and clear to prevent memory issues
entityManager.flush();
entityManager.clear();
}
}
public void batchUpdateResults(List<ValidationResult> results) {
String sql = """
UPDATE validation_results
SET validation_status = :status, error_message = :message, updated_at = CURRENT_TIMESTAMP
WHERE id = :id
""";
// Batch update using native query for better performance
for (ValidationResult result : results) {
entityManager.createNativeQuery(sql)
.setParameter("status", result.getValidationStatus().name())
.setParameter("message", result.getErrorMessage())
.setParameter("id", result.getId())
.executeUpdate();
}
}
}
9. Asynchronous Processing & Messaging
Event-driven boundaries in this feature — what triggers async vs. what stays synchronous, and why
The validation framework will primarily operate synchronously to ensure immediate feedback to the client. However, certain validation operations can be made asynchronous:
- Batch Validation: When a large number of entities need validation (e.g., bulk upload of customer data), the system will process these asynchronously to avoid blocking the main request thread.
- Complex Rule Validation: Some validation rules might involve external service calls or heavy computation that can be offloaded to background workers.
- Audit Trail Generation: Once validation completes, generating audit logs or sending notifications can be done asynchronously to improve response time.
Synchronous operations ensure that validation results are returned immediately to the client, which is critical for real-time validation use cases. Asynchronous operations help with scalability and performance when dealing with large volumes or expensive validations.
Message schema design with a sample payload (Avro / Protobuf / JSON Schema — with versioning strategy)
{
"name": "ValidationEvent",
"type": "record",
"version": "1.0",
"fields": [
{
"name": "eventId",
"type": "string"
},
{
"name": "entityType",
"type": "string"
},
{
"name": "entityId",
"type": "string"
},
{
"name": "validationRules",
"type": {
"type": "array",
"items": {
"name": "ValidationRule",
"type": "record",
"fields": [
{
"name": "ruleId",
"type": "string"
},
{
"name": "ruleName",
"type": "string"
},
{
"name": "ruleType",
"type": "string"
},
{
"name": "parameters",
"type": {
"type": "map",
"values": "string"
}
}
]
}
}
},
{
"name": "timestamp",
"type": "long"
},
{
"name": "status",
"type": "string"
}
]
}
Versioning strategy:
- Schema versioning follows semantic versioning (MAJOR.MINOR.PATCH)
- Backward compatibility is maintained for minor versions
- Major version changes require migration of existing consumers
- Schema registry with version control is used to manage schema evolution
Dead letter queue strategy and poison message handling
Poison messages are handled through a dead letter queue (DLQ) system:
- Retry Mechanism: Messages are retried up to 3 times with exponential backoff (1s, 4s, 9s)
- DLQ: Messages that fail after maximum retries are sent to a DLQ topic
- Manual Intervention: Failed messages in DLQ can be manually inspected and reprocessed
- Alerting: When DLQ threshold is exceeded (e.g., 100 messages), alerts are sent to on-call engineers
At-least-once vs. exactly-once delivery — decision with justification for this feature
At-least-once delivery is chosen for the validation framework because:
- Idempotency: The validation process is designed to be idempotent - running the same validation twice should produce the same result
- Simplicity: At-least-once is simpler to implement and maintain than exactly-once
- Performance: Avoids the complexity and overhead of maintaining transaction state across systems
- Business Requirements: For validation, it's acceptable to have duplicate validations if the system is resilient to such scenarios
Retry policy for consumers (max attempts, backoff formula, non-retryable error classification)
Retry policy configuration:
- Max Attempts: 3 retries
- Backoff Formula: Exponential backoff with jitter (1s, 4s, 9s)
- Jitter: ±20% randomization to prevent thundering herd
- Retryable Errors: Network timeouts, service unavailability, validation rule processing errors
- Non-retryable Errors: Invalid input data, malformed messages, validation rule configuration errors
public class RetryPolicy {
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
private static final double JITTER_FACTOR = 0.2;
public boolean shouldRetry(Exception exception) {
return exception instanceof NetworkException ||
exception instanceof ServiceUnavailableException ||
exception instanceof ValidationProcessingException;
}
public long getDelay(int attempt) {
long delay = (long) Math.pow(BASE_DELAY_MS, attempt);
double jitter = (Math.random() * 2 * JITTER_FACTOR - JITTER_FACTOR) * delay;
return Math.max(100, delay + (long) jitter);
}
}
Outbox pattern for reliable event publishing — include or explicitly justify omission
Outbox pattern is included in the validation framework because:
- Transaction Safety: Ensures that events are published only after successful database operations
- Consistency: Maintains consistency between business logic and event publishing
- Reliability: Prevents lost events in case of system failures during transaction commits
The outbox pattern is implemented by storing validation events in a dedicated validation_events table within the same transaction as the business logic. A separate background worker processes these events and publishes them to the message broker.
Sequence diagram for all async flows (Mermaid)
sequenceDiagram
participant Client
participant ValidationService
participant EventPublisher
participant MessageBroker
participant DLQHandler
Client->>ValidationService: Submit validation request
ValidationService->>ValidationService: Process validation rules synchronously
ValidationService->>EventPublisher: Store validation event in outbox table
EventPublisher->>MessageBroker: Publish validation event to queue
MessageBroker->>ValidationService: Acknowledge receipt
ValidationService->>Client: Return validation results
loop Retry Logic
MessageBroker->>ValidationService: Delivery failure
ValidationService->>EventPublisher: Retry publishing from outbox
end
alt DLQ Handling
ValidationService->>EventPublisher: Move failed events to DLQ
EventPublisher->>DLQHandler: Send to dead letter queue
DLQHandler->>DLQHandler: Alert on DLQ threshold exceeded
end
10. Error Handling Design
Error Classification Hierarchy
The validation framework follows a three-tier error classification system:
- Validation Errors - Input data fails validation rules (e.g., required field missing, format invalid)
- Business Logic Errors - Valid input but violates business constraints (e.g., date in past, insufficient funds)
- Infrastructure Errors - System-level issues (e.g., database connection failure, timeout)
public class ValidationException extends RuntimeException {
private final ValidationErrorType type;
private final String errorCode;
private final Map<String, Object> context;
public ValidationException(ValidationErrorType type, String errorCode, String message) {
super(message);
this.type = type;
this.errorCode = errorCode;
this.context = new HashMap<>();
}
// Getters and setters
}
public enum ValidationErrorType {
VALIDATION_ERROR,
BUSINESS_LOGIC_ERROR,
INFRASTRUCTURE_ERROR
}
Error Propagation Strategy
Errors flow up through the call stack with appropriate wrapping and context addition:
- Validation layer: Catch and wrap raw errors into domain-specific validation exceptions
- Service layer: Handle business logic errors locally or rethrow as validation exceptions
- Controller layer: Catch and translate to appropriate HTTP status codes
Retry Strategy with Exponential Backoff and Jitter
For infrastructure errors that may be transient:
public class RetryableValidationException extends ValidationException {
private final int maxRetries;
private final long baseDelayMs;
private final double jitterFactor;
public RetryableValidationException(String errorCode, String message, int maxRetries, long baseDelayMs) {
super(ValidationErrorType.INFRASTRUCTURE_ERROR, errorCode, message);
this.maxRetries = maxRetries;
this.baseDelayMs = baseDelayMs;
this.jitterFactor = 0.2; // ±20% jitter
}
public long getDelayWithJitter(int attempt) {
long delay = (long) (baseDelayMs * Math.pow(2, attempt));
long jitter = (long) (delay * (Math.random() * 2 * jitterFactor - jitterFactor));
return Math.max(100, delay + jitter);
}
}
Circuit Breaker Design
For external validation services or database connections:
public class ValidationCircuitBreaker {
private final int failureThreshold;
private final long timeoutMs;
private final int resetTimeoutMs;
private volatile CircuitState state = CircuitState.CLOSED;
private int failureCount = 0;
private long lastFailureTime = 0;
public enum CircuitState {
CLOSED, OPEN, HALF_OPEN
}
public boolean canExecute() {
switch (state) {
case CLOSED:
return true;
case HALF_OPEN:
// Allow one request through to test if it succeeds
return true;
case OPEN:
if (System.currentTimeMillis() - lastFailureTime > resetTimeoutMs) {
state = CircuitState.HALF_OPEN;
return true;
}
return false;
}
return false;
}
public void recordSuccess() {
if (state == CircuitState.HALF_OPEN) {
state = CircuitState.CLOSED;
}
}
public void recordFailure() {
lastFailureTime = System.currentTimeMillis();
failureCount++;
if (failureCount >= failureThreshold) {
state = CircuitState.OPEN;
}
}
}
Fallback Behavior Design
public class ValidationFallback {
private final ValidationService primaryService;
private final ValidationService fallbackService;
public ValidationResult validateWithFallback(ValidationRequest request) {
try {
return primaryService.validate(request);
} catch (RetryableValidationException e) {
if (e.getAttempt() < e.getMaxRetries()) {
// Retry logic handled by caller
throw e;
}
// Fallback to secondary validation service or default behavior
return fallbackService.validate(request);
} catch (ValidationException e) {
// Non-retryable errors, fallback or rethrow
return new ValidationResult(false, "Fallback validation failed", Collections.emptyList());
}
}
}
Error Logging Format and Alerting Thresholds
Structured logging with trace context:
public class ValidationLogger {
private static final Logger logger = LoggerFactory.getLogger(ValidationLogger.class);
public void logValidationError(String traceId, String userId, String entityId,
ValidationErrorType type, String errorCode, String message) {
logger.error("Validation error",
"trace_id", traceId,
"user_id", userId,
"entity_id", entityId,
"error_type", type.name(),
"error_code", errorCode,
"message", message);
}
public void logInfrastructureError(String traceId, String userId, String entityId,
String errorCode, String message, Exception cause) {
logger.error("Infrastructure error",
"trace_id", traceId,
"user_id", userId,
"entity_id", entityId,
"error_code", errorCode,
"message", message,
"exception", cause);
}
}
Alerting thresholds:
- Critical: >100 validation errors per minute (alert on 5+ errors in 1 min)
- Warning: >50 business logic errors per minute (alert on 5+ in 1 min)
- Info: >20 retryable infrastructure errors per minute (alert on 3+ in 1 min)
11. Security Design
Input Validation and Sanitization Strategy
Input validation is implemented at multiple layers with a defense-in-depth approach:
- API Layer: All inputs are validated using Jakarta Bean Validation (JSR-380) with custom constraints for business logic validation
- Service Layer: Additional domain-specific validations are performed using custom validation annotations and utility methods
- Database Layer: Input sanitization is enforced through parameterized queries and stored procedure validation
// Custom validation annotation for email format
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
public @interface ValidEmail {
String message() default "Invalid email format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// Email validation implementation
public class EmailValidator implements ConstraintValidator<ValidEmail, String> {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$");
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
if (email == null || email.isEmpty()) return true;
return EMAIL_PATTERN.matcher(email).matches();
}
}
SQL Injection Prevention
All database interactions use parameterized queries and prepared statements exclusively:
// Secure query execution using PreparedStatement
public List<User> findUsersByEmail(String email) {
String sql = "SELECT * FROM users WHERE email = ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, email);
ResultSet rs = stmt.executeQuery();
// Process results
}
}
Authentication Middleware Design
Authentication is handled through JWT-based stateless authentication:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null && tokenProvider.validateToken(token)) {
Authentication auth = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Authorization Enforcement
Role-Based Access Control (RBAC) is implemented with Spring Security:
// Method-level security with role-based checks
@PreAuthorize("hasRole('ADMIN') or @userSecurityService.canAccessUser(principal, userId)")
public void deleteUser(@PathVariable String userId) {
// Implementation
}
// Custom authorization service for complex logic
@Service
public class UserSecurityService {
public boolean canAccessUser(Authentication user, String targetUserId) {
String currentUser = user.getName();
return currentUser.equals(targetUserId) ||
hasRole(user, "ADMIN");
}
}
Sensitive Data Encryption
All sensitive data is encrypted at rest using AES-256:
@Service
public class DataEncryptionService {
private final Cipher cipher;
public DataEncryptionService() throws Exception {
this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
}
public String encrypt(String plaintext) throws Exception {
// Generate key and IV
SecretKey secretKey = generateKey();
byte[] iv = generateIV();
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
byte[] encrypted = cipher.doFinal(plaintext.getBytes());
return Base64.getEncoder().encodeToString(
ByteBuffer.allocate(12 + encrypted.length)
.put(iv)
.put(encrypted)
.array()
);
}
}
PII Handling and Masking Strategy
Personal Identifiable Information is masked in logs and responses:
@Component
public class PiiMasker {
public String maskEmail(String email) {
if (email == null || email.isEmpty()) return email;
int atPosition = email.indexOf('@');
if (atPosition <= 0) return email;
String localPart = email.substring(0, atPosition);
String domain = email.substring(atPosition);
// Mask local part except first and last character
if (localPart.length() <= 2) {
return "*" + domain;
}
return localPart.charAt(0) + "****" +
localPart.charAt(localPart.length() - 1) + domain;
}
}
Secrets Management
Configuration secrets are managed through AWS Secrets Manager:
@Component
public class SecretManager {
private final AWSSecretsManager secretsManager;
public String getSecret(String secretName) {
GetSecretValueRequest request = GetSecretValueRequest.builder()
.secretId(secretName)
.build();
return secretsManager.getSecretValue(request).secretString();
}
}
Audit Logging Design
All security-relevant operations are logged with structured format:
@Component
public class SecurityAuditLogger {
private static final Logger logger = LoggerFactory.getLogger(SecurityAuditLogger.class);
public void logSecurityEvent(String eventType, String userId,
Map<String, Object> additionalData) {
Map<String, Object> auditLog = new HashMap<>();
auditLog.put("timestamp", Instant.now().toString());
auditLog.put("event_type", eventType);
auditLog.put("user_id", userId);
auditLog.put("ip_address", getCurrentIpAddress());
auditLog.put("session_id", getSessionId());
auditLog.put("additional_data", additionalData);
logger.info("SECURITY_AUDIT: {}", auditLog);
}
}
12. Testing Strategy
Unit Testing Scope and Coverage Targets (%)
- Core validation logic: 90%+ code coverage with focus on edge cases and boundary conditions
- Validator implementations: 95%+ coverage including all validation rules and error message generation
- Custom annotation processing: 85%+ coverage for annotation handling and configuration parsing
- Validation context objects: 100% coverage for all getter/setter methods and state transitions
- Integration points: 80%+ coverage for cross-component interactions
Integration Testing Strategy
- Test containers: Use Docker-based test containers for database and cache integration testing (PostgreSQL, Redis)
- Embedded databases: H2 in-memory database for fast unit test execution with schema validation
- WireMock: For external service stubbing and contract testing with HTTP clients
- Test data factories: Factory pattern for generating realistic test data with configurable constraints
- Database transaction isolation: Integration tests run within isolated database transactions to prevent side effects
Contract Testing
- Pact Framework: Consumer-driven contract testing between service boundaries using Pact broker for version management
- API specification validation: Validate against OpenAPI/Swagger specifications using tools like Swagger UI and Postman collections
- Version compatibility testing: Ensure backward compatibility across validation rule versions with automated contract verification
Performance Testing Tool and Key Scenarios
- k6: Load testing using k6 with focus on validation throughput under concurrent requests (1000+ concurrent validations)
- Key scenarios:
- High-volume validation processing (10k/sec validation requests)
- Batch validation operations (1000 items per batch)
- Complex nested object validation (deeply nested structures with multiple constraints)
- Validation rule performance regression testing
- Metrics tracked: P50/P95/P99 response times, throughput, error rates under load
Test Data Management Strategy
- Fixtures: YAML/JSON fixtures for common validation scenarios and error cases
- Factories: Factory classes with configurable parameters for generating test data (e.g., valid/invalid combinations)
- Seeding: Automated database seeding scripts for test environments with pre-defined validation rule configurations
- Data isolation: Per-test transaction rollback to maintain clean test state without database cleanup overhead
- Randomized testing: Property-based testing with libraries like JUnit-Quickcheck to generate edge case inputs automatically
// Example test data factory for validation scenarios
public class ValidationTestDataFactory {
public static ValidationRequest createValidRequest() {
return ValidationRequest.builder()
.id(UUID.randomUUID())
.data("valid-data")
.rules(Arrays.asList("required", "minLength:5"))
.build();
}
public static ValidationRequest createInvalidRequest() {
return ValidationRequest.builder()
.id(UUID.randomUUID())
.data("")
.rules(Arrays.asList("required", "minLength:5"))
.build();
}
}
13. Observability Design
Structured Logging Schema
The logging system uses structured JSON format with consistent fields to enable programmatic analysis and filtering:
{
"timestamp": "2023-10-15T14:30:45.123Z",
"level": "INFO",
"service": "validation-framework",
"trace_id": "12345678-1234-1234-1234-123456789012",
"span_id": "abcdef12-3456-7890-abcd-ef1234567890",
"user_id": "user-12345",
"entity_id": "validation-rule-7890",
"request_id": "req-abcde12345",
"operation": "validateRule",
"message": "Validation rule processed successfully",
"duration_ms": 45,
"error_code": null,
"error_message": null,
"validation_result": {
"passed": true,
"errors": []
},
"pii_masked": false
}
PII masking rules:
- User IDs, email addresses, phone numbers are masked in logs
- All sensitive fields in request/response bodies are redacted or replaced with
*** - Log levels above INFO exclude sensitive fields entirely
Distributed Tracing Instrumentation
Using OpenTelemetry for end-to-end tracing with custom spans:
Key Operations to Trace:
ValidationRule.validate()- Entry point for validationValidationEngine.process()- Core processing logicRuleEvaluator.evaluate()- Individual rule evaluationValidationResult.publish()- Event publishing
Span Attributes:
// Example custom span attributes for validation rule processing
span.setAttribute("validation.rule.id", rule.getId());
span.setAttribute("validation.rule.type", rule.getType());
span.setAttribute("validation.context", context);
span.setAttribute("validation.result.passed", result.isPassed());
span.setAttribute("validation.errors.count", result.getErrors().size());
Required Trace Context Fields:
trace_id- Unique identifier for the entire tracespan_id- Identifier for current operation spanparent_span_id- For child spans
Metrics Design
RED Method Metrics (Rate, Errors, Duration)
| Metric Name | Type | Description | Labels |
|---|---|---|---|
validation_requests_total | Counter | Total validation requests | rule_type, status |
validation_errors_total | Counter | Validation failures | rule_type, error_code |
validation_duration_seconds | Histogram | Validation processing time | rule_type, status |
validation_rules_loaded | Gauge | Current loaded validation rules | rule_type |
validation_cache_hits_total | Counter | Cache hits for validation rules | rule_type |
validation_cache_misses_total | Counter | Cache misses for validation rules | rule_type |
Label Design:
rule_type: "email", "phone", "amount", "date"status: "success", "failure", "timeout"error_code: "INVALID_FORMAT", "MISSING_FIELD", "EXPIRED_RULE"
Health Check Endpoint Design
GET /health
GET /ready
GET /live
GET /startup
Response Format:
{
"status": "healthy",
"timestamp": "2023-10-15T14:30:45.123Z",
"service": "validation-framework",
"version": "1.2.3",
"checks": {
"database": {
"status": "healthy",
"latency_ms": 12
},
"cache": {
"status": "healthy",
"latency_ms": 5
},
"validation_engine": {
"status": "healthy",
"rules_loaded": 128
}
}
}
Alerting Thresholds
| Metric | Warning Level | Critical Level | Escalation |
|---|---|---|---|
validation_errors_total (per minute) | > 10 errors | > 50 errors | On-call engineer |
validation_duration_seconds (p99) | > 500ms | > 2s | On-call engineer |
validation_requests_total (rate) | < 100 req/sec | < 50 req/sec | SRE team |
validation_cache_misses_total (rate) | > 100/sec | > 500/sec | SRE team |
database_connection_pool (utilization) | > 80% | > 95% | SRE team |
Alerting Logic:
- Use sliding window of 5-minute intervals for alert thresholds
- Implement auto-recovery detection for transient issues
- Critical alerts trigger immediate on-call escalation with Slack/Email notifications
14. Open Questions, Risks & Trade-offs
Decisions requiring product / engineering alignment
- Validation scope granularity: Should validation be applied at the API boundary, service layer, or domain object level? The framework needs to support both coarse-grained (API-level) and fine-grained (field-level) validation, but the choice of default scope affects adoption patterns.
- Validation rule inheritance and composition: How should validation rules be composed and inherited across different use cases? Should there be a hierarchical rule system or flat composition model?
- Validation vs. business logic boundary: Where do we draw the line between validation and business rule enforcement? Some validations might be better expressed as domain constraints rather than pure validation rules.
- Framework vs. custom implementation: Should the framework be a pluggable component or an integrated part of the core service architecture? This affects how easily teams can adopt it.
Known trade-offs and justifications
| Trade-off | Option A (Preferred) | Option B | Justification |
|---|---|---|---|
| Performance vs. Flexibility | Single-threaded validation engine with precompiled rules | Multi-threaded validator that compiles rules at runtime | Option A offers better performance and predictable latency but sacrifices runtime flexibility for rule customization |
| Validation Rule Expressiveness | Simple declarative validation rules (e.g., @Min(10), @Email) | Complex rule DSL with custom logic expressions | Option A provides better developer experience and tooling support but limits complex conditional validation scenarios |
| Error Reporting Granularity | Coarse-grained error reporting (list of field names with generic messages) | Fine-grained error reporting (detailed per-rule errors with context) | Option A reduces payload size and improves client-side error handling but sacrifices debugging capability |
| Validation Rule Caching | In-memory caching of validation rule metadata | No caching, recompute rules on every validation | Option A improves performance for repeated validations but adds complexity in cache invalidation and memory usage |
| Validation Context Propagation | Validation context is passed through the validation chain | Validation context is built and maintained by the framework | Option A provides more control but increases boilerplate code; Option B reduces boilerplate at cost of flexibility |
Risks and mitigation strategies
| Risk | Impact | Mitigation Strategy |
|---|---|---|
| Performance degradation | High latency in validation paths, especially for complex nested objects | Implement rule caching, parallel validation for independent fields, and provide performance benchmarks for different validation patterns |
| Validation rule conflicts | Different validation rules might contradict each other or apply in unexpected order | Implement validation rule dependency tracking and provide clear error messages when conflicts are detected |
| Security vulnerabilities | Malicious input could trigger expensive validation logic or cause DoS | Implement timeout limits per validation, rate limiting on validation endpoints, and input size constraints |
| Framework complexity | Complex framework increases maintenance burden and developer onboarding time | Provide comprehensive documentation, clear examples, and progressive complexity in framework usage |
| Validation rule versioning | Changes to validation rules might break existing clients or data | Implement versioned validation schemas and provide backward compatibility guarantees for stable rule sets |
| Caching invalidation | Cached validation rules might become stale due to configuration changes | Implement cache invalidation triggers based on rule configuration updates and provide manual cache clear mechanisms |
Technical debt acknowledged
- Rule definition complexity: The framework's rule definition system might become overly complex with too many configuration options, leading to maintenance overhead and developer confusion.
- Performance monitoring gaps: While the framework provides performance metrics, there's no built-in mechanism to automatically detect and alert on performance regressions in validation rules.
- Testing coverage gaps: The framework's validation logic is hard to unit test in isolation, requiring integration tests that are more expensive to maintain and run.
- Rule composition limitations: The current approach to rule composition might not scale well for highly complex validation scenarios involving conditional logic and cross-field dependencies.
- Context propagation complexity: The context propagation mechanism might introduce subtle bugs in multi-threaded environments where context is not properly isolated between concurrent validations.