The specification pattern is a powerful behavioral design pattern that enables the composition of complex business rules and queries through reusable, composable specifications. This document presents a complete low-level design for implementing a flexible specification engine with support for logical operators, nested specifications, and database query generation.
1. Problem Statement & Requirements
Clarifying questions
- What is the scope of "specifications"? Are we designing a general-purpose specification engine for filtering, validation, or both? Will it support business rules, data validation, or just query composition?
- What scale expectations are there? Will this engine handle millions of concurrent specifications, or is it for a limited set of use cases within a single application?
- What consistency requirements exist? Should specifications be evaluated atomically, or can they be evaluated in a distributed fashion with eventual consistency?
- What's the read/write ratio? Is this primarily used for filtering data (read-heavy) or also for defining and modifying specifications (read/write)?
- What's the latency sensitivity? Is this a real-time system where sub-100ms response is required, or can it tolerate higher latencies?
- What authentication and authorization model is in place? Will the engine support role-based access control for who can define or modify specifications?
- What external systems integrate with this? Will it integrate with other microservices or databases, and what are the reliability expectations?
Functional requirements
- Specification Definition: Users should be able to define specifications using a fluent API or DSL.
- Specification Composition: Support for combining multiple specifications with AND, OR, and NOT operators.
- Specification Evaluation: Evaluate a specification against a domain object or data set to determine if it matches.
- Specification Persistence: Store and retrieve specifications from a database or cache.
- Specification Versioning: Support for versioning of specifications to handle evolution over time.
- Specification Reuse: Allow sharing and reusing specifications across different use cases or services.
Non-functional requirements
- Latency Targets:
- p50: < 5ms for simple specifications
- p95: < 20ms for medium complexity specs
- p99: < 100ms for complex specs with many conditions
- Throughput: Support at least 10,000 evaluations/sec for simple specs under load.
- Availability: 99.9% uptime with graceful degradation for read-heavy workloads.
Constraints and assumptions
- Assumption: The system will primarily be used in a single service or bounded context.
- Constraint: Specifications must be serializable for persistence and transport.
- Assumption: Specifications will not exceed 100 conditions in a single logical unit.
- Constraint: The system will not support runtime specification compilation or JIT.
Out of scope
- External system integration: This engine will not integrate with external systems like Kafka or message queues directly.
- UI/Console: The engine does not include a UI for defining specifications.
- Real-time updates: Specifications will not support real-time updates without explicit reloads or cache invalidation.
- Cross-service specification sharing: Specifications will not be shared across microservices without explicit API boundaries.
- Specification execution in distributed systems: Distributed evaluation of specifications is out of scope.
2. API Design
API Style Selection
REST is selected for this specification engine due to its:
- Widespread adoption and developer familiarity
- Clear resource-based semantics that align well with specification entities
- Built-in HTTP method semantics (GET, POST, PUT, DELETE) for CRUD operations
- Native support for caching and browser compatibility
- Established tooling ecosystem (Swagger, Postman, etc.)
Endpoint Definitions
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /v1/specifications | List all specifications with filtering and pagination | JWT |
| POST | /v1/specifications | Create a new specification | JWT |
| GET | /v1/specifications/{id} | Retrieve a specific specification by ID | JWT |
| PUT | /v1/specifications/{id} | Update an existing specification | JWT |
| DELETE | /v1/specifications/{id} | Soft delete a specification | JWT |
| POST | /v1/specifications/{id}/evaluate | Evaluate a specification against input data | JWT |
| GET | /v1/specifications/{id}/history | Get specification version history | JWT |
Request/Response Schemas
Create Specification Request
{
"name": "Product Price Validation",
"description": "Validates that product price is within acceptable range",
"specificationType": "PRICE_VALIDATION",
"conditions": [
{
"field": "price",
"operator": "GREATER_THAN_OR_EQUAL",
"value": 0
},
{
"field": "price",
"operator": "LESS_THAN_OR_EQUAL",
"value": 10000
}
],
"metadata": {
"category": "pricing",
"owner": "pricing-team"
}
}
Create Specification Response
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Product Price Validation",
"description": "Validates that product price is within acceptable range",
"specificationType": "PRICE_VALIDATION",
"conditions": [
{
"field": "price",
"operator": "GREATER_THAN_OR_EQUAL",
"value": 0
},
{
"field": "price",
"operator": "LESS_THAN_OR_EQUAL",
"value": 10000
}
],
"status": "ACTIVE",
"createdAt": "2023-10-01T10:00:00Z",
"updatedAt": "2023-10-01T10:00:00Z",
"version": 1
}
Evaluate Specification Request
{
"inputData": {
"price": 150,
"category": "electronics",
"currency": "USD"
}
}
Evaluate Specification Response
{
"specificationId": "123e4567-e89b-12d3-a456-426614174000",
"evaluationResult": {
"passed": true,
"reason": "All conditions met",
"details": [
{
"condition": {
"field": "price",
"operator": "GREATER_THAN_OR_EQUAL",
"value": 0
},
"passed": true,
"message": "Price is greater than or equal to 0"
}
]
},
"evaluatedAt": "2023-10-01T10:05:00Z"
}
HTTP Status Codes and Error Response Format
| Status Code | Description |
|---|---|
| 200 | Successful GET, PUT, DELETE |
| 201 | Successful POST |
| 400 | Bad Request (validation errors) |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Resource Not Found |
| 409 | Conflict (e.g., duplicate specification name) |
| 422 | Unprocessable Entity (validation errors) |
| 500 | Internal Server Error |
Standardized error response:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data provided",
"details": [
{
"field": "price",
"message": "Price must be a positive number"
}
],
"timestamp": "2023-10-01T10:00:00Z"
}
}
API Versioning Strategy
Versioned using path prefix: /v1/ with future support for header-based versioning (Accept: application/vnd.spec-engine.v2+json). Versioning is done per major version to maintain backward compatibility for minor versions.
Pagination Design
Cursor-based pagination is chosen over offset-based because:
- More efficient for large datasets
- Consistent results even when data changes during pagination
- Better performance characteristics
{
"data": [/* specification objects */],
"pagination": {
"cursor": "eyJpZCI6IjEyM2U0NTY3LWU4OWItMTJkMy1hNDU2LTQyNjYxNDE3NDAwMCJ9",
"hasNext": true,
"hasPrevious": false
}
}
Authentication and Authorization
All endpoints require JWT authentication with scopes:
specifications:read- Read access to specificationsspecifications:write- Create/update/delete specificationsspecifications:evaluate- Evaluate specifications
Authorization follows RBAC model where users are assigned roles that grant appropriate scopes.
Rate Limiting
Rate limiting per endpoint:
GET /v1/specifications: 100 requests/minutePOST /v1/specifications: 50 requests/minutePOST /v1/specifications/{id}/evaluate: 200 requests/minute
Implementation uses token bucket algorithm with distributed Redis-based rate limiting.
Idempotency Design
For mutation endpoints (POST, PUT), idempotency keys are supported:
Request header:
X-Idempotency-Key: unique-key-for-this-request
If the same key is used within 5 minutes, the system returns the previous response instead of reprocessing.
// Example idempotency key handling in service layer
public Specification createSpecification(SpecificationRequest request, String idempotencyKey) {
if (idempotencyKey != null) {
// Check if this key was used recently
String existingResult = idempotencyCache.get(idempotencyKey);
if (existingResult != null) {
return deserialize(existingResult);
}
}
// Process the request
Specification spec = processRequest(request);
if (idempotencyKey != null) {
// Cache the result for 5 minutes
idempotencyCache.put(idempotencyKey, serialize(spec), Duration.ofMinutes(5));
}
return spec;
}
3. Data Model Design
Entity-Relationship diagram (Mermaid)
erDiagram
SPECIFICATION {
string id PK
string name
string description
string category
boolean isActive
timestamp createdAt
timestamp updatedAt
string createdBy
string updatedBy
}
SPECIFICATION_CRITERIA {
string id PK
string specificationId FK
string field
string operator
string value
string valueType
timestamp createdAt
}
SPECIFICATION_GROUP {
string id PK
string name
string description
timestamp createdAt
}
SPECIFICATION_GROUP_MEMBER {
string groupId FK
string specificationId FK
timestamp createdAt
}
SPECIFICATION {
string id PK
}
SPECIFICATION_GROUP {
string id PK
}
SPECIFICATION_GROUP_MEMBER {
string groupId FK
string specificationId FK
}
SPECIFICATION ||--o{ SPECIFICATION_CRITERIA : "has"
SPECIFICATION_GROUP ||--o{ SPECIFICATION_GROUP_MEMBER : "contains"
SPECIFICATION_GROUP_MEMBER ||--o{ SPECIFICATION : "belongs to"
Table / Collection schema definitions with field types, constraints, indices
-- Specifications table
CREATE TABLE specifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
category VARCHAR(100),
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) NOT NULL,
updated_by VARCHAR(255),
-- Indexes
INDEX idx_specifications_category (category),
INDEX idx_specifications_active (is_active),
INDEX idx_specifications_created_at (created_at)
);
-- Specification criteria table
CREATE TABLE specification_criteria (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
specification_id UUID NOT NULL REFERENCES specifications(id) ON DELETE CASCADE,
field VARCHAR(255) NOT NULL,
operator VARCHAR(50) NOT NULL, -- eq, neq, gt, gte, lt, lte, in, not_in, contains, starts_with, ends_with
value TEXT NOT NULL,
value_type VARCHAR(50) NOT NULL, -- string, number, boolean, date, list
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Indexes
INDEX idx_specification_criteria_specification (specification_id),
INDEX idx_specification_criteria_field (field),
INDEX idx_specification_criteria_operator (operator)
);
-- Specification groups table
CREATE TABLE specification_groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Indexes
INDEX idx_specification_groups_name (name),
INDEX idx_specification_groups_created_at (created_at)
);
-- Specification group membership table
CREATE TABLE specification_group_members (
group_id UUID NOT NULL REFERENCES specification_groups(id) ON DELETE CASCADE,
specification_id UUID NOT NULL REFERENCES specifications(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Composite primary key
PRIMARY KEY (group_id, specification_id),
-- Indexes
INDEX idx_specification_group_members_group (group_id),
INDEX idx_specification_group_members_specification (specification_id)
);
Primary key strategy (UUID vs. auto-increment vs. ULID — with justification)
Using UUID as primary key for all entities:
Justification:
- Global uniqueness: UUIDs ensure uniqueness across distributed systems and databases without coordination
- Scalability: No need for centralized ID generation, enabling horizontal scaling of databases
- Security: UUIDs don't expose sequential information that could be exploited for enumeration attacks
- Database sharding: UUIDs work well with sharded databases where sequential IDs would cause hotspots
- Cross-system integration: UUIDs can be generated in any system and safely merged without ID conflicts
Index design (composite, partial, covering indices — with justification per index)
-
Composite indexes:
idx_specification_criteria_specification_field(specification_id, field): For efficient querying of criteria within a specificationidx_specification_group_members_group_specification(group_id, specification_id): For efficient group membership lookups
-
Partial indexes:
idx_specifications_active_category(category) WHERE is_active = TRUE: For fast filtering of active specifications by categoryidx_specification_criteria_field_operator(field, operator) WHERE value_type = 'string': For optimized string-based criteria queries
-
Covering indexes:
idx_specifications_active_name(id, name, is_active) WHERE is_active = TRUE: Covers queries that need to fetch specification metadata for active specs
Data partitioning / sharding strategy
Partition by category field in the specifications table:
- Shard key:
category - Partitioning strategy: Hash-based sharding on category values
- Shard size: 100 categories per shard (configurable)
- Cross-shard queries: Supported through distributed query coordination layer
- Shard management: Automatic rebalancing during scaling operations
Soft delete vs. hard delete decision with justification
Soft delete using is_active boolean field:
Justification:
- Auditability: Retains historical information about specifications that were removed
- Recovery capability: Allows for easy restoration of deleted specifications
- Referential integrity: Maintains data consistency when specifications are referenced in other systems
- Compliance: Supports regulatory requirements for data retention
- Analytics: Enables analysis of specification lifecycle and usage patterns
Audit trail design (who, what, when)
Audit information stored in dedicated fields:
created_byandupdated_by: User identifier (email or user ID)created_atandupdated_at: Timestamps with timezone informationis_active: Tracks specification state changes
Data archival strategy and retention policy
- Archival trigger: Specifications marked as inactive for > 365 days
- Archive storage: Separate archive database or table with reduced indexing
- Retention policy:
- Active specifications: 5 years
- Inactive specifications: 10 years (with read-only access)
- Archive query: Dedicated read-only endpoint for archived specifications
- Cleanup job: Monthly process to move expired specifications to archive
4. Class Design & Service Layer
UML Class Diagram
classDiagram
class Specification {
<<Interface>>
+isSatisfiedBy(T item)
+and(Specification other)
+or(Specification other)
+not()
}
class ConcreteSpecification {
-predicate
+isSatisfiedBy(T item)
+and(Specification other)
+or(Specification other)
+not()
}
class AndSpecification {
-left
-right
+isSatisfiedBy(T item)
+and(Specification other)
+or(Specification other)
+not()
}
class OrSpecification {
-left
-right
+isSatisfiedBy(T item)
+and(Specification other)
+or(Specification other)
+not()
}
class NotSpecification {
-spec
+isSatisfiedBy(T item)
+and(Specification other)
+or(Specification other)
+not()
}
class Product {
-id
-name
-price
-category
+getId()
+getName()
+getPrice()
+getCategory()
}
class ProductSpecificationService {
-productRepository
+findProductsBySpec(Specification~Product~ spec)
+isProductValid(Product product)
}
Specification <|-- ConcreteSpecification
Specification <|-- AndSpecification
Specification <|-- OrSpecification
Specification <|-- NotSpecification
ConcreteSpecification o-- Product : applies to
ProductSpecificationService --> Specification : uses
ProductSpecificationService --> Product : operates on
State Machine Diagram for Specification
stateDiagram-v2
[*] --> Ready
Ready --> Active : activate()
Active --> Inactive : deactivate()
Inactive --> Active : reactivate()
Active --> [*] : delete()
Service Class Structure and Responsibilities
The ProductSpecificationService is responsible for:
- Applying business logic specifications to product entities
- Combining multiple specifications using logical operators (AND, OR, NOT)
- Enforcing specification-based filtering and validation
- Managing the lifecycle of specifications in the system
Interface / Abstract Class Design
public interface Specification<T> {
boolean isSatisfiedBy(T item);
default Specification<T> and(Specification<T> other) {
return new AndSpecification<>(this, other);
}
default Specification<T> or(Specification<T> other) {
return new OrSpecification<>(this, other);
}
default Specification<T> not() {
return new NotSpecification<>(this);
}
}
public abstract class AbstractSpecification<T> implements Specification<T> {
@Override
public Specification<T> and(Specification<T> other) {
return new AndSpecification<>(this, other);
}
@Override
public Specification<T> or(Specification<T> other) {
return new OrSpecification<>(this, other);
}
@Override
public Specification<T> not() {
return new NotSpecification<>(this);
}
}
Business Logic Encapsulation Patterns
The Specification pattern uses the Strategy pattern to encapsulate different business rules. Each concrete specification implements a specific rule, and the composition of specifications allows complex business logic to be built from simple components.
Critical-Path Method Implementation
public class ProductSpecificationService {
private final ProductRepository productRepository;
private final EventBus eventBus;
public List<Product> findProductsBySpec(Specification<Product> spec) {
return productRepository.findAll().stream()
.filter(spec::isSatisfiedBy)
.collect(Collectors.toList());
}
public boolean isProductValid(Product product) {
// Validate price range
Specification<Product> priceSpec = new AbstractSpecification<>() {
@Override
public boolean isSatisfiedBy(Product item) {
return item.getPrice() >= 0 && item.getPrice() <= 10000;
}
};
// Validate category
Specification<Product> categorySpec = new AbstractSpecification<>() {
@Override
public boolean isSatisfiedBy(Product item) {
return Arrays.asList("Electronics", "Clothing", "Books").contains(item.getCategory());
}
};
// Combine specifications
Specification<Product> validationSpec = priceSpec.and(categorySpec);
return validationSpec.isSatisfiedBy(product);
}
}
Transaction Boundary Design
The specification engine operates at the service layer and does not require explicit transaction boundaries since it's a read/query operation. For write operations involving specification validation, the transaction boundary is determined by the calling service layer and typically includes:
- Specification validation checks
- Entity persistence operations
- Event publishing for audit trails
Service Interface Contracts
public interface ProductSpecificationService {
List<Product> findProductsBySpec(Specification<Product> spec);
boolean isProductValid(Product product);
void saveSpecification(Specification<Product> spec, String name);
Specification<Product> loadSpecification(String name);
}
Saga / Orchestration Pattern
The specification engine does not require a saga pattern since it's primarily a query and validation system. However, if specifications were to trigger complex workflows (e.g., product approval processes), a saga pattern would be appropriate for managing distributed transactions.
Domain Object / Value Object Design
public class Product {
private final UUID id;
private final String name;
private final BigDecimal price;
private final String category;
public Product(UUID id, String name, BigDecimal price, String category) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.price = Objects.requireNonNull(price);
this.category = Objects.requireNonNull(category);
}
// Getters and equals/hashCode
}
Concrete Specification Implementations
public class PriceRangeSpecification extends AbstractSpecification<Product> {
private final BigDecimal minPrice;
private final BigDecimal maxPrice;
public PriceRangeSpecification(BigDecimal minPrice, BigDecimal maxPrice) {
this.minPrice = Objects.requireNonNull(minPrice);
this.maxPrice = Objects.requireNonNull(maxPrice);
}
@Override
public boolean isSatisfiedBy(Product item) {
return item.getPrice().compareTo(minPrice) >= 0 &&
item.getPrice().compareTo(maxPrice) <= 0;
}
}
public class CategorySpecification extends AbstractSpecification<Product> {
private final Set<String> allowedCategories;
public CategorySpecification(Set<String> allowedCategories) {
this.allowedCategories = Objects.requireNonNull(allowedCategories);
}
@Override
public boolean isSatisfiedBy(Product item) {
return allowedCategories.contains(item.getCategory());
}
}
Composite Specification Implementation
public class AndSpecification<T> extends AbstractSpecification<T> {
private final Specification<T> left;
private final Specification<T> right;
public AndSpecification(Specification<T> left, Specification<T> right) {
this.left = Objects.requireNonNull(left);
this.right = Objects.requireNonNull(right);
}
@Override
public boolean isSatisfiedBy(T item) {
return left.isSatisfiedBy(item) && right.isSatisfiedBy(item);
}
}
public class OrSpecification<T> extends AbstractSpecification<T> {
private final Specification<T> left;
private final Specification<T> right;
public OrSpecification(Specification<T> left, Specification<T> right) {
this.left = Objects.requireNonNull(left);
this.right = Objects.requireNonNull(right);
}
@Override
public boolean isSatisfiedBy(T item) {
return left.isSatisfiedBy(item) || right.isSatisfiedBy(item);
}
}
public class NotSpecification<T> extends AbstractSpecification<T> {
private final Specification<T> spec;
public NotSpecification(Specification<T> spec) {
this.spec = Objects.requireNonNull(spec);
}
@Override
public boolean isSatisfiedBy(T item) {
return !spec.isSatisfiedBy(item);
}
}
5. Concurrency & Thread Safety
Concurrency Model
The Specification pattern engine operates in a multi-threaded environment where multiple concurrent requests may evaluate specifications simultaneously. The system uses a thread-safe approach with minimal shared mutable state:
- Thread model: Java's standard thread model with synchronized access to shared resources
- Worker threads: Each specification evaluation runs in its own thread, with no thread pooling required for the core logic
- Immutable specification objects: Specifications are designed to be immutable, ensuring thread safety without explicit synchronization
Race Condition Prevention Strategy
Race conditions are mitigated through:
- Immutable Specification Objects: All specification components (expressions, operators, criteria) are immutable once constructed
- Thread-local Evaluation Context: Each evaluation thread maintains its own local context for runtime values and parameters
- No Shared Mutable State: Specifications are evaluated in isolation; no shared mutable state between concurrent evaluations
Optimistic vs. Pessimistic Locking Selection
The Specification pattern engine does not require locking mechanisms because:
- No shared mutable state: Specifications are read-only and immutable
- Evaluation is stateless: Each evaluation operates on independent data structures
- No concurrent modification: Specifications are not modified during evaluation
- Thread-safe design: The pattern is inherently designed for concurrent access
Deadlock Prevention Strategy
Deadlock prevention is not a concern in this design because:
- No interdependent operations: Specifications are evaluated independently
- No shared locks: No shared resources or locks are acquired during evaluation
- Single-threaded evaluation context: Each specification evaluation runs in isolation
Idempotency Implementation for Concurrent Duplicate Requests
Idempotency is not a primary concern for the Specification pattern engine since:
- Read-only operations: Specifications are evaluated, not modified
- Deterministic outcomes: Same input always produces same result
- No side effects: Specification evaluation does not modify external state
However, for systems that might cache or store specification results:
// Example of idempotent caching pattern for specification evaluation
public class IdempotentSpecificationEvaluator {
private final Cache<String, Boolean> evaluationCache =
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(5))
.build();
public boolean evaluate(Specification<?> spec, Object context) {
String cacheKey = generateCacheKey(spec, context);
return evaluationCache.get(cacheKey, () -> {
// Expensive evaluation logic here
return spec.isSatisfiedBy(context);
});
}
private String generateCacheKey(Specification<?> spec, Object context) {
return spec.getClass().getName() + ":" +
context.hashCode() + ":" +
System.currentTimeMillis();
}
}
Thread Safety Considerations
- Specification object immutability: All specification components are immutable
- Context isolation: Each evaluation thread has its own context object
- No shared mutable state: No shared variables between concurrent evaluations
- Thread-safe collections: When storing or managing specifications, use thread-safe collections like
ConcurrentHashMap
Performance Characteristics
- High concurrency support: The design allows for thousands of concurrent specification evaluations
- Low memory overhead: No thread synchronization primitives or locks required
- Scalable: The pattern scales linearly with CPU cores without contention
Implementation Notes
// Example of thread-safe specification evaluation in a multi-threaded environment
public class ThreadSafeSpecificationEngine {
private final List<Specification<?>> specifications;
public ThreadSafeSpecificationEngine(List<Specification<?>> specs) {
// Defensive copy to ensure immutability
this.specifications = Collections.unmodifiableList(specs);
}
// Thread-safe evaluation - no synchronization needed
public boolean evaluateAll(Object context) {
return specifications.stream()
.allMatch(spec -> spec.isSatisfiedBy(context));
}
// Thread-safe filtering - no synchronization needed
public <T> List<T> filter(List<T> items, Specification<T> spec) {
return items.stream()
.filter(item -> spec.isSatisfiedBy(item))
.collect(Collectors.toList());
}
}
6. Sequence Diagrams for Key Flows
sequenceDiagram
participant Client
participant SpecificationEngine
participant SpecificationRepository
participant Database
Client->>SpecificationEngine: POST /specifications
SpecificationEngine->>SpecificationRepository: Save specification
SpecificationRepository->>Database: Insert specification record
Database-->>SpecificationRepository: Return saved spec
SpecificationRepository-->>SpecificationEngine: Return spec ID
SpecificationEngine-->>Client: 201 Created with spec ID
Client->>SpecificationEngine: GET /specifications/{id}
SpecificationEngine->>SpecificationRepository: Find by ID
SpecificationRepository->>Database: Query specification
Database-->>SpecificationRepository: Return spec record
SpecificationRepository-->>SpecificationEngine: Return spec object
SpecificationEngine-->>Client: 200 OK with spec data
Client->>SpecificationEngine: POST /specifications/{id}/evaluate
SpecificationEngine->>SpecificationRepository: Load specification
SpecificationRepository->>Database: Query specification by ID
Database-->>SpecificationRepository: Return spec record
SpecificationRepository-->>SpecificationEngine: Return spec object
SpecificationEngine->>SpecificationEngine: Evaluate specification against data
SpecificationEngine-->>Client: 200 OK with evaluation result
sequenceDiagram
participant Client
participant SpecificationEngine
participant SpecificationRepository
participant Database
participant EventPublisher
Client->>SpecificationEngine: POST /specifications
SpecificationEngine->>SpecificationRepository: Save specification
SpecificationRepository->>Database: Insert specification record
Database-->>SpecificationRepository: Return saved spec
SpecificationRepository-->>SpecificationEngine: Return spec ID
SpecificationEngine->>EventPublisher: Publish specification.created event
EventPublisher-->>SpecificationEngine: Event published successfully
SpecificationEngine-->>Client: 201 Created with spec ID
Client->>SpecificationEngine: DELETE /specifications/{id}
SpecificationEngine->>SpecificationRepository: Find specification
SpecificationRepository->>Database: Query specification by ID
Database-->>SpecificationRepository: Return spec record
SpecificationRepository-->>SpecificationEngine: Return spec object
SpecificationEngine->>SpecificationRepository: Soft delete specification
SpecificationRepository->>Database: Update spec with deleted_at timestamp
Database-->>SpecificationRepository: Return updated spec
SpecificationRepository-->>SpecificationEngine: Return success status
SpecificationEngine->>EventPublisher: Publish specification.deleted event
EventPublisher-->>SpecificationEngine: Event published successfully
SpecificationEngine-->>Client: 204 No Content
Client->>SpecificationEngine: POST /specifications/{id}/evaluate
SpecificationEngine->>SpecificationRepository: Load specification (active only)
SpecificationRepository->>Database: Query active spec by ID
Database-->>SpecificationRepository: Return spec record
SpecificationRepository-->>SpecificationEngine: Return spec object
SpecificationEngine->>SpecificationEngine: Evaluate specification against data
SpecificationEngine-->>Client: 200 OK with evaluation result
Client->>SpecificationEngine: POST /specifications/{id}/evaluate
SpecificationEngine->>SpecificationRepository: Load specification (active only)
SpecificationRepository->>Database: Query spec by ID with deleted_at IS NULL
Database-->>SpecificationRepository: Return empty result (404)
SpecificationRepository-->>SpecificationEngine: Return not found error
SpecificationEngine-->>Client: 404 Not Found
Client->>SpecificationEngine: POST /specifications/{id}/evaluate
SpecificationEngine->>SpecificationRepository: Load specification (active only)
SpecificationRepository->>Database: Query spec by ID with deleted_at IS NULL
Database-->>SpecificationRepository: Return spec record
SpecificationRepository-->>SpecificationEngine: Return spec object
SpecificationEngine->>SpecificationEngine: Evaluate specification against data (validation error)
SpecificationEngine-->>Client: 400 Bad Request with validation error details
sequenceDiagram
participant Client
participant SpecificationEngine
participant SpecificationRepository
participant Database
participant EventPublisher
participant AsyncWorker
Client->>SpecificationEngine: POST /specifications
SpecificationEngine->>SpecificationRepository: Save specification
SpecificationRepository->>Database: Insert specification record
Database-->>SpecificationRepository: Return saved spec
SpecificationRepository-->>SpecificationEngine: Return spec ID
SpecificationEngine->>EventPublisher: Publish specification.created event to queue
EventPublisher-->>SpecificationEngine: Event queued successfully
SpecificationEngine-->>Client: 201 Created with spec ID
EventPublisher->>AsyncWorker: Consume specification.created event
AsyncWorker->>SpecificationRepository: Load specification from DB
SpecificationRepository->>Database: Query specification by ID
Database-->>SpecificationRepository: Return spec record
SpecificationRepository-->>AsyncWorker: Return spec object
AsyncWorker->>AsyncWorker: Validate specification syntax
AsyncWorker-->>AsyncWorker: Log validation result (success/failure)
AsyncWorker->>EventPublisher: Publish specification.validated event
EventPublisher-->>AsyncWorker: Event published successfully
AsyncWorker->>AsyncWorker: Update spec status in DB (status = 'validated')
AsyncWorker->>SpecificationRepository: Update specification status
SpecificationRepository->>Database: Update spec status field
Database-->>SpecificationRepository: Return success
SpecificationRepository-->>AsyncWorker: Return updated spec
AsyncWorker-->>AsyncWorker: Log status update completion
AsyncWorker->>EventPublisher: Publish specification.status.changed event
EventPublisher-->>AsyncWorker: Event published successfully
7. Caching Design
Cache Strategy Selection
For the Specification pattern engine, we'll use a cache-aside pattern with a distributed Redis cluster as the primary caching layer. This approach provides:
- Explicit cache control: Cache operations are explicitly managed by the application
- Scalability: Redis cluster can scale horizontally to handle high throughput
- Persistence options: Redis supports various persistence modes for durability requirements
- Rich data structures: Support for complex cache data types needed for specification caching
Cache Key Design
Cache keys will follow a structured namespace pattern:
{namespace}:{entity_type}:{specification_id}:{version}:{hash}
Example key structure:
spec:product:12345:v1:abc123def456
Namespace strategy:
specfor specification-related cache entriesspec:productfor product-specific specificationsspec:orderfor order-related specifications
Cache TTL Strategy
| Data Type | TTL | Reason |
|---|---|---|
| Specification definitions | 24h | Specifications change infrequently, can be refreshed via cache invalidation |
| Specification evaluation results | 5m | Recent evaluations should remain fresh, but older results can be recomputed |
| Specification metadata | 1h | Metadata about specifications (tags, categories) can be cached for moderate duration |
| Specification validation results | 10m | Validation outcomes should be reasonably fresh but not overly strict |
Cache Eviction Policy
LRU (Least Recently Used) with TTL-based expiration as the primary eviction strategy:
- LRU: Evicts least recently accessed items when cache is full
- TTL: Ensures items don't live longer than their validity period
- Priority eviction: Higher-priority specifications (e.g., frequently used) are less likely to be evicted
Cache Stampede Prevention
To prevent cache stampedes:
-
Probabilistic early expiry: Set cache TTL to 80% of actual TTL (e.g., 4 minutes for 5-minute cache)
-
Distributed locks: Use Redis distributed locks for cache warming operations:
public class CacheLock { private static final String LOCK_PREFIX = "spec:lock:"; private static final int LOCK_TTL_MS = 5000; public boolean acquireLock(String key) { String lockKey = LOCK_PREFIX + key; return redisTemplate.opsForValue() .setIfAbsent(lockKey, "locked", Duration.ofMillis(LOCK_TTL_MS)); } } -
Cache warming: Pre-warm frequently accessed specifications during low-traffic periods
Distributed Cache Architecture
Redis Cluster Configuration:
- 3 master nodes with 3 replica nodes each
- Automatic failover with Redis Sentinel monitoring
- Cross-datacenter replication for high availability
- Memory eviction policy set to
allkeys-lru
Cache Invalidation Strategy
Event-driven invalidation with cache tags:
-
Tag-based invalidation: Each cached specification has associated tags (e.g.,
product,category:electronics) -
Event-driven invalidation: When specifications change, publish cache invalidation events:
@EventListener public void handleSpecificationUpdate(SpecificationUpdatedEvent event) { String tag = "spec:" + event.getSpecificationType(); redisTemplate.delete(tag); } -
Immediate invalidation: Invalidate cache entries when specification definitions are updated or deleted
Cache Read/Write Patterns
Cache-Aside Pattern Implementation:
public class SpecificationCache {
private final RedisTemplate<String, Object> redisTemplate;
public SpecificationResult evaluateWithCache(
String specId,
Map<String, Object> context) {
String cacheKey = buildCacheKey(specId, context);
SpecificationResult cached = getCachedResult(cacheKey);
if (cached != null) {
return cached;
}
// Cache miss - compute result
SpecificationResult result = evaluateSpecification(specId, context);
// Cache the result with TTL
cacheResult(cacheKey, result);
return result;
}
private SpecificationResult getCachedResult(String key) {
Object cached = redisTemplate.opsForValue().get(key);
return cached != null ? (SpecificationResult) cached : null;
}
private void cacheResult(String key, SpecificationResult result) {
redisTemplate.opsForValue()
.set(key, result, Duration.ofMinutes(5));
}
}
Cache Performance Considerations
- Cache hit ratio targets: Aim for 85%+ cache hit ratio for frequently accessed specifications
- Memory allocation: Allocate 60-70% of Redis memory to specification data, 30% for metadata and tags
- Cache warming: Implement background warming of hot specifications during off-peak hours
- Monitoring: Track cache hit/miss ratios and eviction rates to optimize TTLs and memory allocation
Cache Validation Strategy
- Consistency checks: Validate that cached specification results match current evaluation logic
- Versioning: Include specification version in cache keys to prevent stale results from older versions
- Cache health monitoring: Monitor Redis cluster health and cache performance metrics to detect degradation early
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 that defines the contract for data operations.
public interface SpecificationRepository {
Specification save(Specification specification);
Optional<Specification> findById(UUID id);
List<Specification> findAll();
List<Specification> findByCriteria(SpecificationCriteria criteria);
void delete(UUID id);
boolean existsById(UUID id);
}
public interface SpecificationValueRepository {
List<SpecificationValue> saveAll(List<SpecificationValue> values);
List<SpecificationValue> findBySpecificationId(UUID specificationId);
void deleteBySpecificationId(UUID specificationId);
}
ORM vs. Raw Query Decisions
For the Specification engine, we use JPA/Hibernate as the ORM for its type safety and mapping capabilities. Raw queries are used for complex analytical operations and performance-critical paths where ORM joins become inefficient.
@Repository
public class SpecificationRepositoryImpl implements SpecificationRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Specification save(Specification specification) {
if (specification.getId() == null) {
entityManager.persist(specification);
} else {
entityManager.merge(specification);
}
return specification;
}
@Override
public Optional<Specification> findById(UUID id) {
return Optional.ofNullable(entityManager.find(Specification.class, id));
}
@Override
public List<Specification> findByCriteria(SpecificationCriteria criteria) {
// Using JPQL for complex filtering
String jpql = "SELECT s FROM Specification s WHERE s.status = :status";
TypedQuery<Specification> query = entityManager.createQuery(jpql, Specification.class);
query.setParameter("status", criteria.getStatus());
if (criteria.getCategory() != null) {
query.setParameter("category", criteria.getCategory());
}
return query.getResultList();
}
}
Read vs. Write Separation
We implement CQRS (Command Query Responsibility Segregation) for the Specification engine to separate read and write operations. This allows us to optimize each side independently.
public interface SpecificationReadRepository {
List<SpecificationView> findActiveSpecifications();
SpecificationView findById(UUID id);
List<SpecificationView> findByCategory(String category);
}
public interface SpecificationWriteRepository {
Specification save(Specification specification);
void delete(UUID id);
}
Connection Pool Configuration
Connection pool is configured with HikariCP for optimal performance:
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
validation-timeout: 5000
N+1 Query Prevention Strategy
Using eager loading and batch fetching to prevent N+1 queries:
@Entity
@Table(name = "specifications")
@NamedEntityGraph(
name = "Specification.withValues",
attributeNodes = @NamedAttributeNode(value = "values", fetchMode = FetchType.EAGER)
)
public class Specification {
@OneToMany(mappedBy = "specification", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@BatchSize(size = 20)
private List<SpecificationValue> values = new ArrayList<>();
}
Batch Operation Design
Batch operations are implemented using bulk operations where possible:
@Repository
public class SpecificationValueRepositoryImpl {
@PersistenceContext
private EntityManager entityManager;
public void batchDeleteBySpecificationIds(List<UUID> specificationIds) {
String jpql = "DELETE FROM SpecificationValue sv WHERE sv.specification.id IN :ids";
entityManager.createQuery(jpql)
.setParameter("ids", specificationIds)
.executeUpdate();
}
public void batchSave(List<SpecificationValue> values) {
for (int i = 0; i < values.size(); i++) {
entityManager.persist(values.get(i));
if (i % 50 == 0) { // Flush every 50 entities
entityManager.flush();
entityManager.clear();
}
}
}
}
9. Asynchronous Processing & Messaging
Event-driven boundaries in this feature
The Specification pattern engine is primarily a synchronous validation and filtering system. However, it can benefit from asynchronous processing in two key scenarios:
- Specification Updates: When a specification is updated or created, we want to invalidate cached results and notify dependent systems.
- Bulk Specification Validation: For large-scale validation of entities against specifications (e.g., batch validating all products in a catalog), we want to process these asynchronously to avoid blocking the main request path.
The synchronous operations (like validating a single entity against a specification) remain synchronous to ensure immediate feedback and consistency. Asynchronous flows are used for system-wide updates and bulk operations.
Message schema design with sample payload
We'll use JSON Schema for message payloads, versioned to support future evolution:
{
"version": "1.0",
"event_type": "specification.updated",
"timestamp": "2023-10-27T10:00:00Z",
"payload": {
"specification_id": "uuid-12345",
"updated_fields": ["name", "rules"],
"previous_version": "v1.0",
"new_version": "v2.0"
},
"source": "specification-engine",
"correlation_id": "correlation-67890"
}
Versioning strategy uses semantic versioning with major version bumps for breaking changes and minor versions for backward-compatible additions.
Dead letter queue strategy and poison message handling
We implement a dead-letter queue (DLQ) approach for failed message consumption:
- Failed messages are moved to a
specification-engine.dlqtopic after 3 retries - Poison messages (messages that consistently fail) are logged with full context and escalated to monitoring systems
- DLQ messages are manually reprocessed after root cause investigation
At-least-once vs. exactly-once delivery
We choose at-least-once delivery for specification events because:
- Specification updates are idempotent - reprocessing the same update event has no side effects
- The system can tolerate duplicate events for cache invalidation and audit logging
- The complexity of exactly-once semantics (transactional publishing, idempotent consumers) outweighs the benefits for this use case
Retry policy for consumers
Retry policy configuration:
- Max retries: 3 attempts
- Backoff strategy: Exponential with jitter (1s, 2s, 4s ± 1s)
- Non-retryable errors: Invalid specification format, missing required fields
- Retryable errors: Network timeouts, temporary service unavailability
Outbox pattern for reliable event publishing
We do not use the outbox pattern in this design because:
- The specification engine is a single-service system with no distributed transactions
- Events are published after successful database commits (using transactional outbox pattern implicitly)
- The system can tolerate eventual consistency in event delivery for cache invalidation
Sequence diagram for async flows
sequenceDiagram
participant Client
participant SpecEngine
participant EventBus
participant CacheService
participant DLQ
Client->>SpecEngine: Create/Update Specification
SpecEngine->>SpecEngine: Validate and Save to DB
SpecEngine->>EventBus: Publish SpecificationUpdatedEvent
EventBus->>CacheService: Invalidate specification cache
EventBus->>SpecEngine: Acknowledge successful publish
SpecEngine-->>Client: Return success response
Note over EventBus,DLQ: Retry logic with DLQ fallback
EventBus->>EventBus: Retry up to 3 times
EventBus->>DLQ: Move to DLQ on failure
10. Error Handling Design
Error Classification Hierarchy
The error handling system follows a three-tier hierarchy to enable proper error categorization and response:
- Domain Errors - Business logic violations (e.g., invalid specification, unsupported operation)
- Infrastructure Errors - System-level issues (e.g., database connection failure, cache timeout)
- Validation Errors - Input validation failures (e.g., malformed JSON, missing required fields)
public abstract class BaseException extends RuntimeException {
private final String errorCode;
private final int httpStatus;
public BaseException(String errorCode, int httpStatus, String message) {
super(message);
this.errorCode = errorCode;
this.httpStatus = httpStatus;
}
public String getErrorCode() { return errorCode; }
public int getHttpStatus() { return httpStatus; }
}
public class SpecificationException extends BaseException {
public SpecificationException(String message) {
super("SPECIFICATION_ERROR", 400, message);
}
}
public class ValidationException extends BaseException {
public ValidationException(String message) {
super("VALIDATION_ERROR", 400, message);
}
}
public class InfrastructureException extends BaseException {
public InfrastructureException(String message) {
super("INFRASTRUCTURE_ERROR", 500, message);
}
}
Error Propagation Strategy
Errors are handled at the appropriate layer:
- Domain errors are caught and re-thrown as domain-specific exceptions
- Infrastructure errors are logged and wrapped in infrastructure exceptions for consistent handling
- Validation errors are handled at the API boundary with immediate HTTP 400 responses
Retry Strategy with Exponential Backoff and Jitter
For transient infrastructure errors, a retry mechanism is implemented with exponential backoff:
public class RetryableOperation {
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
private static final double JITTER_FACTOR = 0.2;
public <T> T executeWithRetry(Supplier<T> operation) {
int attempts = 0;
while (attempts < MAX_RETRIES) {
try {
return operation.get();
} catch (InfrastructureException e) {
attempts++;
if (attempts >= MAX_RETRIES) throw e;
long delay = (long) (BASE_DELAY_MS * Math.pow(2, attempts));
delay += (long) (delay * JITTER_FACTOR * (Math.random() - 0.5));
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new InfrastructureException("Retry interrupted");
}
}
}
throw new InfrastructureException("Max retries exceeded");
}
}
Circuit Breaker Design
A circuit breaker pattern is implemented to prevent cascading failures:
public class CircuitBreaker {
private static final int FAILURE_THRESHOLD = 5;
private static final long TIMEOUT_MS = 60000;
private static final int RESET_TIMEOUT_MS = 30000;
private volatile CircuitState state = CircuitState.CLOSED;
private int failureCount = 0;
private long lastFailureTime = 0;
public enum CircuitState {
CLOSED, OPEN, HALF_OPEN
}
public <T> T execute(Supplier<T> operation) {
switch (state) {
case CLOSED:
try {
T result = operation.get();
handleSuccess();
return result;
} catch (Exception e) {
handleFailure(e);
throw e;
}
case OPEN:
if (System.currentTimeMillis() - lastFailureTime > RESET_TIMEOUT_MS) {
state = CircuitState.HALF_OPEN;
return operation.get();
}
throw new InfrastructureException("Circuit breaker open");
case HALF_OPEN:
try {
T result = operation.get();
state = CircuitState.CLOSED;
return result;
} catch (Exception e) {
state = CircuitState.OPEN;
throw e;
}
}
throw new IllegalStateException("Invalid circuit state");
}
private void handleSuccess() {
failureCount = 0;
}
private void handleFailure(Exception e) {
if (e instanceof InfrastructureException) {
failureCount++;
lastFailureTime = System.currentTimeMillis();
if (failureCount >= FAILURE_THRESHOLD) {
state = CircuitState.OPEN;
}
}
}
}
Fallback Behavior Design
Fallbacks are implemented for external dependencies:
public class ExternalDependencyService {
private final CircuitBreaker circuitBreaker = new CircuitBreaker();
public SpecificationResult getSpecificationFromExternal(String id) {
return circuitBreaker.execute(() -> {
// External service call
try {
return externalSpecificationService.fetch(id);
} catch (Exception e) {
// Fallback to local cache or default
return getSpecificationFromCache(id);
}
});
}
private SpecificationResult getSpecificationFromCache(String id) {
// Cache lookup logic
return cache.get(id);
}
}
Error Logging Format and Alerting Thresholds
Structured logging is implemented with consistent fields:
public class ErrorLogger {
private static final Logger logger = LoggerFactory.getLogger(ErrorLogger.class);
public void logError(String operation, String errorCode, Exception exception,
Map<String, Object> context) {
Map<String, Object> logFields = new HashMap<>();
logFields.put("timestamp", Instant.now());
logFields.put("operation", operation);
logFields.put("errorCode", errorCode);
logFields.put("errorMessage", exception.getMessage());
logFields.put("stackTrace", exception.getStackTrace());
logFields.put("context", context);
logger.error("{}", logFields);
}
}
Alerting thresholds:
- Critical: >50 infrastructure errors per minute
- Warning: >20 validation errors per minute
- Info: >5 domain errors per hour
Error Response Format
Standardized JSON error responses:
{
"error": {
"code": "SPECIFICATION_ERROR",
"message": "Invalid specification format",
"httpStatus": 400,
"timestamp": "2023-10-01T12:00:00Z",
"traceId": "abc-123-def"
}
}
11. Security Design
Input Validation and Sanitization Strategy
All API inputs undergo strict validation using a combination of:
- Custom validation annotations for business rule enforcement (e.g.,
@ValidSpecificationName,@ValidOperator) - Regular expression patterns for format validation (e.g., alphanumeric + underscore for specification names)
- Length constraints to prevent buffer overflow attacks
- Type validation using strict type checking (e.g.,
Integervs.Stringfor IDs)
public class SpecificationValidator {
private static final Pattern SPEC_NAME_PATTERN =
Pattern.compile("^[a-zA-Z0-9_]{1,100}$");
public static void validateSpecificationName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new ValidationException("Specification name cannot be null or empty");
}
if (!SPEC_NAME_PATTERN.matcher(name).matches()) {
throw new ValidationException(
"Specification name must be 1-100 alphanumeric characters or underscores");
}
}
}
SQL Injection Prevention
All database queries use parameterized statements exclusively:
- JPA/Hibernate with named parameters for all dynamic queries
- No string concatenation in SQL - all inputs are bound as parameters
- Whitelisting of allowed column names for dynamic sorting/filtering
@Repository
public class SpecificationRepository {
@PersistenceContext
private EntityManager entityManager;
public List<Specification> findSpecificationsByCriteria(
String name,
SpecificationOperator operator,
String value) {
String jpql = "SELECT s FROM Specification s WHERE s.name = :name " +
"AND s.operator = :operator AND s.value = :value";
return entityManager.createQuery(jpql, Specification.class)
.setParameter("name", name)
.setParameter("operator", operator)
.setParameter("value", value)
.getResultList();
}
}
Authentication Middleware Design
Authentication is handled via JWT Bearer tokens with:
- OAuth2 Authorization Code flow for user login
- JWT token validation in a global filter (
SecurityFilter) - Token expiration handling with automatic refresh for valid sessions
- Role-based access control enforced at the service layer
@Component
public class SecurityFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String token = extractToken(httpRequest);
if (token != null && validateToken(token)) {
String userId = extractUserId(token);
SecurityContext context = new SecurityContext(userId);
SecurityContextHolder.setContext(context);
}
chain.doFilter(request, response);
}
}
Authorization Enforcement
RBAC (Role-Based Access Control) is implemented with:
- User roles:
ADMIN,USER,ANONYMOUS - Permission mapping in a centralized policy service
- Fine-grained authorization checks before critical operations
@Service
public class SpecificationAuthorizationService {
private final Set<String> ADMIN_ROLES = Set.of("ADMIN");
public boolean canModifySpecification(String userId, Specification spec) {
if (isAdmin(userId)) {
return true;
}
// Check ownership or shared access
return spec.getOwnerId().equals(userId);
}
private boolean isAdmin(String userId) {
// Implementation to fetch user roles from cache or DB
return false;
}
}
Sensitive Data Encryption
All sensitive specification data is encrypted at rest:
- AES-256-GCM encryption for specification values
- Key rotation policy: 90-day key rotation with automatic re-encryption of stored data
- HSM integration for key storage in production environments
@Component
public class SpecificationEncryptionService {
private final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
public String encrypt(String plaintext, String key) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encrypted = cipher.doFinal(plaintext.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
}
}
PII Handling and Masking Strategy
PII in specification data is:
- Identified and tagged using a PII classification system
- Masked in logs and responses using pattern-based redaction (e.g., "123-456-7890" → "XXX-XXX-7890")
- Compliance with GDPR/CCPA through data retention policies
public class PiiMaskingService {
public String maskPii(String input) {
// Mask phone numbers
return input.replaceAll("\\b(\\d{3})-?(\\d{3})-?(\\d{4})\\b",
"XXX-XXX-$3");
}
}
Secrets Management
Sensitive configuration values (e.g., encryption keys, external API secrets) are:
- Managed via AWS Secrets Manager in production
- Rotated every 90 days with automated key rotation workflows
- Access controlled via IAM policies - only specific services can read secrets
@Component
public class SecretManager {
private final AWSSecretsManager client =
AWSSecretsManagerClientBuilder.defaultClient();
public String getSecret(String secretName) {
GetSecretValueRequest request =
new GetSecretValueRequest().withSecretId(secretName);
return client.getSecretValue(request).getSecretString();
}
}
Audit Logging Design
All security-relevant events are logged with:
- Required fields:
timestamp,userId,action,resourceId,ipAddress - PII masking in log entries
- Structured JSON format for easy parsing and analysis
@Component
public class SecurityAuditLogger {
private static final Logger logger = LoggerFactory.getLogger("SECURITY_AUDIT");
public void logAccessAttempt(String userId, String action,
String resourceId, String ipAddress) {
Map<String, Object> auditEvent = new HashMap<>();
auditEvent.put("timestamp", Instant.now().toString());
auditEvent.put("userId", userId);
auditEvent.put("action", action);
auditEvent.put("resourceId", resourceId);
auditEvent.put("ipAddress", maskIpAddress(ipAddress));
logger.info("Security event: {}", auditEvent);
}
}
12. Testing Strategy
Unit Testing Scope and Coverage Targets (%)
- Core domain logic: 90% code coverage with 100% branch coverage for all business rules and validation logic
- Specification engine: 95% coverage including all specification types (conjunction, disjunction, negation, atomic)
- Service layer: 85% coverage with focus on edge cases and error conditions
- Repository layer: 80% coverage for all query patterns and data access methods
- Integration points: 70% coverage for external service interactions (mocked where appropriate)
Integration Testing Strategy
- Database integration: Test with embedded H2 in-memory database for fast execution and clean state per test
- External service mocks: Use WireMock or Mockito to simulate external API calls (e.g., payment gateway, notification service)
- Cross-service communication: Use Testcontainers for real database and Redis instances when testing distributed flows
- End-to-end workflows: Validate complete specification evaluation paths with real data fixtures
Contract Testing
- API contract testing: Use Pact for consumer-driven contracts between service and external dependencies
- Event schema validation: Implement schema registry integration (e.g., Confluent Schema Registry) with contract tests for all published events
- Version compatibility: Test backward compatibility of specification engine API with older clients using contract testing
Performance Testing Tool and Key Scenarios
- Tool: k6 for load testing with realistic concurrent specification evaluation scenarios
- Key scenarios:
- Concurrent specification evaluation (1000 simultaneous requests)
- Large composite specifications (500+ nested conditions)
- High-frequency specification updates with cache invalidation
- Batch evaluation of 1000+ entities with same specification
Test Data Management Strategy
- Fixtures: Use JSON/YAML fixtures to define test specifications with various complexity levels (atomic, conjunctions, nested)
- Factories: Implement factory patterns for generating valid specification objects with randomized but valid parameters
- Seeding: Use database seed scripts to populate test data sets with realistic business entities (products, users, orders)
- Isolation: Ensure each test runs in isolated database transaction with clean state and proper cleanup after execution
// Example unit test for specification engine
@Test
public void testConjunctionSpecificationEvaluation() {
// Given
Specification<Product> spec1 = new AtomicSpecification<>("price", Operator.GREATER_THAN, 100);
Specification<Product> spec2 = new AtomicSpecification<>("category", Operator.EQUALS, "electronics");
Specification<Product> conjunction = new ConjunctionSpecification<>(Arrays.asList(spec1, spec2));
Product product = new Product(150, "electronics", "Smartphone");
// When
boolean result = conjunction.isSatisfiedBy(product);
// Then
assertTrue(result);
}
13. Observability Design
Structured Logging Schema
All logs follow a structured JSON format with consistent fields to enable programmatic analysis and filtering:
{
"timestamp": "2023-10-27T14:30:45.123Z",
"level": "INFO",
"service": "specification-engine",
"trace_id": "1234567890abcdef",
"span_id": "abcdef1234567890",
"user_id": "user-123",
"entity_id": "spec-456",
"operation": "evaluateSpecification",
"message": "Specification evaluation completed successfully",
"duration_ms": 45,
"error_code": null,
"error_message": null,
"stack_trace": null
}
PII masking is applied to sensitive fields:
user_id: Masked asuser-***email: Masked as***@***.com
Distributed Tracing Instrumentation
OpenTelemetry is used for distributed tracing with custom spans:
Key Spans:
specification.evaluate- Root span for specification evaluationspecification.validate- Validation of input specificationsspecification.compile- Compilation of specification to executable formspecification.execute- Execution of compiled specification
Span Attributes:
specification.id: Unique identifier for the spec being evaluatedspecification.type: Type of specification (e.g., "product", "user")specification.version: Version of the specificationevaluation.context: Context object passed to evaluationevaluation.result: Boolean result of evaluation
Metrics Design
RED Metrics (Rate, Errors, Duration):
| Metric Name | Type | Description | Labels |
|---|---|---|---|
specification_evaluations_total | Counter | Total number of specification evaluations | type, status |
specification_evaluation_errors_total | Counter | Count of failed specification evaluations | type, error_code |
specification_evaluation_duration_seconds | Histogram | Duration of specification evaluation | type, status |
specification_compilation_errors_total | Counter | Count of compilation failures | type, error_code |
specification_cache_hits_total | Counter | Cache hits for compiled specifications | type |
specification_cache_misses_total | Counter | Cache misses for compiled specifications | type |
Gauge Metrics:
specification_cache_size- Current size of compiled specification cacheactive_specifications_count- Number of active specifications in system
Health Check Endpoint Design
/health
Purpose: Liveness check to verify service is running and responding
{
"status": "healthy",
"timestamp": "2023-10-27T14:30:45Z",
"checks": {
"database": "healthy",
"cache": "healthy"
}
}
/ready
Purpose: Readiness check to verify service is ready to serve requests
{
"status": "ready",
"timestamp": "2023-10-27T14:30:45Z",
"dependencies": {
"database": "connected",
"cache": "connected"
}
}
/live
Purpose: Readiness check for critical dependencies
{
"status": "alive",
"timestamp": "2023-10-27T14:30:45Z",
"uptime_seconds": 3600
}
Alerting Thresholds
Critical Alerts (require on-call escalation):
specification_evaluation_errors_total> 10 errors/minute for 5 minutesspecification_evaluation_duration_secondsp99 > 500ms for 10 minutesspecification_cache_misses_total> 500 misses/minute for 2 minutes
Warning Alerts:
specification_evaluation_errors_total> 5 errors/minute for 3 minutesspecification_evaluation_duration_secondsp99 > 200ms for 5 minutesspecification_cache_hit_ratio< 80% for 10 minutes
Alerting Configuration:
- Severity: Critical (on-call), Warning (team)
- Notification Channels: Slack, Email, PagerDuty
- Escalation Path:
- First alert → Team lead
- Second alert → On-call engineer
- Third alert → Engineering manager
14. Open Questions, Risks & Trade-offs
Decisions requiring product / engineering alignment
- Specification composition strategy: Should we support AND/OR/NOT composition through fluent API or AST-based composition? This impacts the complexity of runtime evaluation and developer experience.
- Runtime vs. compile-time validation: Should specifications be pre-compiled into optimized evaluation trees or evaluated dynamically? This affects performance characteristics and deployment complexity.
- Specification versioning strategy: How do we handle backward compatibility when specifications change? Should we version at the specification level or use a more granular approach like field-level versioning?
- Specification persistence model: Should we store specifications as serialized JSON/Protobuf or maintain a normalized database schema for better querying and maintenance?
Known trade-offs and justifications
- Flexibility vs. Performance: The Specification pattern's flexibility in composing complex rules comes at the cost of runtime evaluation overhead. For high-throughput systems, we may need to pre-compile or cache frequently used specifications.
- Type safety vs. extensibility: Using generic types in specification definitions provides compile-time safety but reduces runtime flexibility. We're choosing to favor extensibility for broader use cases at the cost of some type safety.
- Single responsibility vs. composition complexity: While each specification implements a single rule, complex business logic requires composition of multiple specifications, increasing the cognitive load for developers.
- Memory vs. CPU trade-off: Storing pre-compiled specification trees in memory provides faster evaluation but increases memory consumption. We're leaning toward caching for frequently accessed specifications.
Risks and mitigation strategies
- Specification evaluation performance: Complex nested specifications could cause performance bottlenecks. Mitigation includes: implementing specification caching, providing performance metrics, and offering optimization hints to users.
- Specification validation complexity: Users might create invalid specifications that cause runtime errors. Mitigation includes: comprehensive unit tests, specification validation during creation, and providing clear error messages with examples.
- Specification evolution: Changes to specification logic could break existing business rules. Mitigation includes: versioned specifications, backward compatibility guarantees, and automated migration tools.
- Security risks from user-defined specifications: Users could create malicious specifications that cause denial-of-service or data corruption. Mitigation includes: specification sandboxing, resource limits, and input validation.
Technical debt acknowledged
- Specification serialization complexity: The current JSON serialization approach may not scale well for very large or deeply nested specifications, requiring future refactoring to use binary formats or streaming.
- Limited extensibility for custom operators: While the pattern supports custom specifications, extending with new operators requires code changes rather than simple configuration, creating technical debt for future customization needs.
- No built-in specification documentation: Specifications lack metadata for documentation generation, requiring manual effort to maintain user-facing documentation.
- Testing coverage gaps: Complex specification compositions are difficult to test comprehensively, leading to potential edge-case bugs that may surface in production.
- Specification caching invalidation: Cache invalidation logic for specifications that change frequently could be complex and error-prone, potentially leading to stale cached evaluations.