Skip to main content

Specification Pattern Engine Design and Implementation

Comprehensive low-level design for a flexible specification pattern engine supporting complex business rule composition and query building.

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

  1. 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?
  2. 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?
  3. What consistency requirements exist? Should specifications be evaluated atomically, or can they be evaluated in a distributed fashion with eventual consistency?
  4. What's the read/write ratio? Is this primarily used for filtering data (read-heavy) or also for defining and modifying specifications (read/write)?
  5. What's the latency sensitivity? Is this a real-time system where sub-100ms response is required, or can it tolerate higher latencies?
  6. What authentication and authorization model is in place? Will the engine support role-based access control for who can define or modify specifications?
  7. 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

MethodPathDescriptionAuth
GET/v1/specificationsList all specifications with filtering and paginationJWT
POST/v1/specificationsCreate a new specificationJWT
GET/v1/specifications/{id}Retrieve a specific specification by IDJWT
PUT/v1/specifications/{id}Update an existing specificationJWT
DELETE/v1/specifications/{id}Soft delete a specificationJWT
POST/v1/specifications/{id}/evaluateEvaluate a specification against input dataJWT
GET/v1/specifications/{id}/historyGet specification version historyJWT

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 CodeDescription
200Successful GET, PUT, DELETE
201Successful POST
400Bad Request (validation errors)
401Unauthorized
403Forbidden
404Resource Not Found
409Conflict (e.g., duplicate specification name)
422Unprocessable Entity (validation errors)
500Internal 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 specifications
  • specifications:write - Create/update/delete specifications
  • specifications: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/minute
  • POST /v1/specifications: 50 requests/minute
  • POST /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:

  1. Global uniqueness: UUIDs ensure uniqueness across distributed systems and databases without coordination
  2. Scalability: No need for centralized ID generation, enabling horizontal scaling of databases
  3. Security: UUIDs don't expose sequential information that could be exploited for enumeration attacks
  4. Database sharding: UUIDs work well with sharded databases where sequential IDs would cause hotspots
  5. 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)

  1. Composite indexes:

    • idx_specification_criteria_specification_field (specification_id, field): For efficient querying of criteria within a specification
    • idx_specification_group_members_group_specification (group_id, specification_id): For efficient group membership lookups
  2. Partial indexes:

    • idx_specifications_active_category (category) WHERE is_active = TRUE: For fast filtering of active specifications by category
    • idx_specification_criteria_field_operator (field, operator) WHERE value_type = 'string': For optimized string-based criteria queries
  3. 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:

  1. Shard key: category
  2. Partitioning strategy: Hash-based sharding on category values
  3. Shard size: 100 categories per shard (configurable)
  4. Cross-shard queries: Supported through distributed query coordination layer
  5. Shard management: Automatic rebalancing during scaling operations

Soft delete vs. hard delete decision with justification

Soft delete using is_active boolean field:

Justification:

  1. Auditability: Retains historical information about specifications that were removed
  2. Recovery capability: Allows for easy restoration of deleted specifications
  3. Referential integrity: Maintains data consistency when specifications are referenced in other systems
  4. Compliance: Supports regulatory requirements for data retention
  5. Analytics: Enables analysis of specification lifecycle and usage patterns

Audit trail design (who, what, when)

Audit information stored in dedicated fields:

  • created_by and updated_by: User identifier (email or user ID)
  • created_at and updated_at: Timestamps with timezone information
  • is_active: Tracks specification state changes

Data archival strategy and retention policy

  1. Archival trigger: Specifications marked as inactive for > 365 days
  2. Archive storage: Separate archive database or table with reduced indexing
  3. Retention policy:
    • Active specifications: 5 years
    • Inactive specifications: 10 years (with read-only access)
  4. Archive query: Dedicated read-only endpoint for archived specifications
  5. 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:

  1. Immutable Specification Objects: All specification components (expressions, operators, criteria) are immutable once constructed
  2. Thread-local Evaluation Context: Each evaluation thread maintains its own local context for runtime values and parameters
  3. 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:

  1. Read-only operations: Specifications are evaluated, not modified
  2. Deterministic outcomes: Same input always produces same result
  3. 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:

  • spec for specification-related cache entries
  • spec:product for product-specific specifications
  • spec:order for order-related specifications

Cache TTL Strategy

Data TypeTTLReason
Specification definitions24hSpecifications change infrequently, can be refreshed via cache invalidation
Specification evaluation results5mRecent evaluations should remain fresh, but older results can be recomputed
Specification metadata1hMetadata about specifications (tags, categories) can be cached for moderate duration
Specification validation results10mValidation 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:

  1. LRU: Evicts least recently accessed items when cache is full
  2. TTL: Ensures items don't live longer than their validity period
  3. Priority eviction: Higher-priority specifications (e.g., frequently used) are less likely to be evicted

Cache Stampede Prevention

To prevent cache stampedes:

  1. Probabilistic early expiry: Set cache TTL to 80% of actual TTL (e.g., 4 minutes for 5-minute cache)

  2. 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));
        }
    }
  3. 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:

  1. Tag-based invalidation: Each cached specification has associated tags (e.g., product, category:electronics)

  2. Event-driven invalidation: When specifications change, publish cache invalidation events:

    @EventListener
    public void handleSpecificationUpdate(SpecificationUpdatedEvent event) {
        String tag = "spec:" + event.getSpecificationType();
        redisTemplate.delete(tag);
    }
  3. 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

  1. Cache hit ratio targets: Aim for 85%+ cache hit ratio for frequently accessed specifications
  2. Memory allocation: Allocate 60-70% of Redis memory to specification data, 30% for metadata and tags
  3. Cache warming: Implement background warming of hot specifications during off-peak hours
  4. Monitoring: Track cache hit/miss ratios and eviction rates to optimize TTLs and memory allocation

Cache Validation Strategy

  1. Consistency checks: Validate that cached specification results match current evaluation logic
  2. Versioning: Include specification version in cache keys to prevent stale results from older versions
  3. 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:

  1. Specification Updates: When a specification is updated or created, we want to invalidate cached results and notify dependent systems.
  2. 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.dlq topic 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:

  1. Specification updates are idempotent - reprocessing the same update event has no side effects
  2. The system can tolerate duplicate events for cache invalidation and audit logging
  3. 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:

  1. The specification engine is a single-service system with no distributed transactions
  2. Events are published after successful database commits (using transactional outbox pattern implicitly)
  3. 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:

  1. Domain Errors - Business logic violations (e.g., invalid specification, unsupported operation)
  2. Infrastructure Errors - System-level issues (e.g., database connection failure, cache timeout)
  3. 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., Integer vs. String for 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 as user-***
  • email: Masked as ***@***.com

Distributed Tracing Instrumentation

OpenTelemetry is used for distributed tracing with custom spans:

Key Spans:

  1. specification.evaluate - Root span for specification evaluation
  2. specification.validate - Validation of input specifications
  3. specification.compile - Compilation of specification to executable form
  4. specification.execute - Execution of compiled specification

Span Attributes:

  • specification.id: Unique identifier for the spec being evaluated
  • specification.type: Type of specification (e.g., "product", "user")
  • specification.version: Version of the specification
  • evaluation.context: Context object passed to evaluation
  • evaluation.result: Boolean result of evaluation

Metrics Design

RED Metrics (Rate, Errors, Duration):

Metric NameTypeDescriptionLabels
specification_evaluations_totalCounterTotal number of specification evaluationstype, status
specification_evaluation_errors_totalCounterCount of failed specification evaluationstype, error_code
specification_evaluation_duration_secondsHistogramDuration of specification evaluationtype, status
specification_compilation_errors_totalCounterCount of compilation failurestype, error_code
specification_cache_hits_totalCounterCache hits for compiled specificationstype
specification_cache_misses_totalCounterCache misses for compiled specificationstype

Gauge Metrics:

  • specification_cache_size - Current size of compiled specification cache
  • active_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 minutes
  • specification_evaluation_duration_seconds p99 > 500ms for 10 minutes
  • specification_cache_misses_total > 500 misses/minute for 2 minutes

Warning Alerts:

  • specification_evaluation_errors_total > 5 errors/minute for 3 minutes
  • specification_evaluation_duration_seconds p99 > 200ms for 5 minutes
  • specification_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.

Was this article helpful?

Topics in this article

More from Backend LLD

  • Backend LLD

    State Machine Design Patterns and Implementation

    Comprehensive low-level design guide for implementing robust state machines with event-driven transitions and persistence capabilities.

  • Backend LLD

    Observer-Based Event System Design

    Comprehensive low-level design for a scalable observer pattern event system with publish-subscribe capabilities and thread-safe operations.

  • Backend LLD

    Command Pattern Framework Design and Implementation

    Comprehensive low-level design for a flexible command pattern framework with support for undo/redo, logging, and extensible command execution.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.