Skip to main content

Middleware Pipeline Design Patterns and Implementation

Comprehensive low-level design for building extensible middleware pipelines with configurable execution order and error handling.

A middleware pipeline architecture enables flexible request processing by chaining configurable components that can modify, filter, or handle requests and responses in a well-defined sequence.

1. Problem Statement & Requirements

Clarifying questions

  1. What specific middleware functionality are we designing for? (e.g., authentication, logging, rate limiting, request/response transformation)
  2. What is the expected scale of concurrent requests and throughput requirements?
  3. Are there specific protocols or formats (HTTP, gRPC, WebSockets) the middleware pipeline must support?
  4. What are the key entities and their relationships in the system that will be processed by this middleware?
  5. What is the expected latency sensitivity for different types of middleware operations (e.g., auth checks vs. logging)?
  6. Do we need to support configuration-driven middleware chains or hardcoded pipeline definitions?
  7. What are the specific security requirements for middleware components (e.g., encryption, token validation)?

Functional requirements

  • Support configurable middleware pipeline with ordered execution
  • Allow middleware components to modify request/response objects
  • Enable conditional middleware execution based on criteria (path, headers, etc.)
  • Support both synchronous and asynchronous middleware components
  • Provide hooks for pre-processing, post-processing, and error handling
  • Allow middleware to short-circuit the pipeline (early return)
  • Support middleware composition and reuse across different pipelines
  • Enable runtime configuration changes to middleware order or activation

Non-functional requirements

  • Latency targets: p50 < 10ms, p95 < 50ms, p99 < 200ms for typical middleware operations
  • Throughput: 100,000+ concurrent requests per second with 99.9% success rate
  • Availability: 99.99% uptime with graceful degradation capability
  • Scalability: Horizontal scaling support for microservices architecture
  • Extensibility: Support for 50+ middleware components with minimal performance overhead

Constraints and assumptions

  • Middleware pipeline must support both HTTP and gRPC protocols
  • All middleware components must be stateless for horizontal scaling
  • Middleware execution order must be configurable at runtime without code changes
  • Assumption: Middleware components will be implemented in Java (Spring Boot) or Go
  • Assumption: Pipeline configuration is stored in a centralized config service (e.g., Consul, Config Server)
  • Assumption: Middleware components will be deployed as separate services or modules

Out of scope

  • Full API gateway functionality (e.g., rate limiting, circuit breaking, caching)
  • Middleware component implementation details (business logic of specific middleware)
  • Authentication and authorization implementation (middleware will handle validation but not policy enforcement)
  • Middleware-specific logging formats or audit trails (handled by individual middleware components)
  • Integration with specific service discovery or load balancing systems
  • Middleware performance benchmarking and tuning (focus on design, not optimization)
  • Support for middleware in non-HTTP protocols beyond gRPC and HTTP
  • Middleware versioning and rollback strategies

2. API Design

API Style Selection

For a middleware pipeline system, I'll select gRPC as the primary API style for the following reasons:

  1. Strong typing: gRPC's Protocol Buffers provide clear, versioned contracts that are essential for middleware systems where components must interoperate reliably
  2. Performance: Binary serialization is more efficient than JSON for middleware traffic, especially in high-throughput scenarios
  3. Bidirectional streaming: Middleware often requires real-time communication between components
  4. Service contracts: gRPC services provide a clear interface contract that can be versioned and evolved systematically
  5. Built-in error handling: gRPC's standardized error codes and metadata support make it ideal for middleware systems that need to propagate errors gracefully

API Endpoints

MethodPathDescriptionAuthRate Limit
CreatePipeline/v1/pipelinesCreate a new middleware pipeline configurationJWT100 req/min
GetPipeline/v1/pipelines/{id}Retrieve pipeline configuration by IDJWT500 req/min
UpdatePipeline/v1/pipelines/{id}Update existing pipeline configurationJWT50 req/min
DeletePipeline/v1/pipelines/{id}Soft delete pipeline configurationJWT25 req/min
ListPipelines/v1/pipelinesList all pipelines with filtering and paginationJWT200 req/min
StartPipeline/v1/pipelines/{id}/startActivate a pipeline for processingJWT50 req/min
StopPipeline/v1/pipelines/{id}/stopDeactivate a pipeline from processingJWT50 req/min
GetPipelineStatus/v1/pipelines/{id}/statusGet current processing status of pipelineJWT1000 req/min
GetPipelineMetrics/v1/pipelines/{id}/metricsRetrieve performance metrics for pipelineJWT200 req/min
ValidatePipeline/v1/pipelines/validateValidate pipeline configuration before creationJWT100 req/min

Request and Response Schemas

CreatePipelineRequest

{
  "name": "string",
  "description": "string",
  "stages": [
    {
      "name": "string",
      "type": "TRANSFORM | FILTER | ROUTE | LOG",
      "config": {
        "key": "value"
      },
      "timeoutMs": 5000,
      "retryPolicy": {
        "maxRetries": 3,
        "backoffMultiplier": 2,
        "initialDelayMs": 100
      }
    }
  ],
  "errorHandling": {
    "strategy": "FAIL | CONTINUE | SKIP",
    "fallbackStage": "string"
  },
  "inputTopic": "string",
  "outputTopic": "string"
}

CreatePipelineResponse

{
  "id": "uuid",
  "name": "string",
  "status": "CREATED | ACTIVE | INACTIVE",
  "createdAt": "timestamp",
  "updatedAt": "timestamp"
}

PipelineStatusResponse

{
  "id": "uuid",
  "status": "ACTIVE | INACTIVE | ERROR | PAUSED",
  "processingCount": 0,
  "errorCount": 0,
  "lastProcessedAt": "timestamp",
  "uptimeSeconds": 0
}

HTTP Status Codes and Error Response Format

Status CodeDescription
200Success
201Created
400Bad Request - Invalid input
401Unauthorized - Missing or invalid auth
403Forbidden - Insufficient permissions
404Not Found - Resource doesn't exist
429Too Many Requests - Rate limit exceeded
500Internal Server Error - Unexpected system error

Standardized error response format:

{
  "error": {
    "code": "PIPELINE_VALIDATION_ERROR",
    "message": "Pipeline configuration validation failed",
    "details": [
      {
        "field": "stages[0].type",
        "message": "Invalid stage type: TRANSFORM"
      }
    ],
    "timestamp": "2023-12-01T10:30:00Z",
    "requestId": "uuid"
  }
}

API Versioning Strategy

Versioning will be done via path prefix:

  • /v1/ - Current stable version
  • Future versions will use /v2/, /v3/ etc.

Version negotiation will be handled by the API gateway that routes requests to appropriate service versions.

Pagination Design

Cursor-based pagination will be used for the ListPipelines endpoint:

Query Parameters:

  • cursor: Base64 encoded cursor for next page
  • limit: Number of items per page (default: 20, max: 100)
  • sort: Sort field (created_at, name)
  • order: Sort order (ASC or DESC)

Response Format:

{
  "items": [ /* pipeline objects */ ],
  "nextCursor": "base64encodedcursor",
  "hasMore": true,
  "totalCount": 1000
}

Justification: Cursor-based pagination is preferred over offset-based because:

  1. It's more efficient for large datasets
  2. It handles data changes gracefully (inserts/deletes don't shift page boundaries)
  3. It provides better performance for large datasets

Authentication and Authorization

Authentication:

  • JWT tokens with HS256 signing
  • Token expiration: 1 hour
  • Refresh token support (30 days)

Authorization:

  • RBAC model with roles:
    • PIPELINE_ADMIN: Full access to all pipelines
    • PIPELINE_EDITOR: Read/write access to owned pipelines
    • PIPELINE_VIEWER: Read-only access to pipelines

Rate Limiting Strategy

Rate limiting will be implemented per endpoint with:

  • Fixed window algorithm for simplicity and predictability
  • Per-user limits to prevent abuse
  • Sliding window for high-volume endpoints

Idempotency Design

For mutation endpoints, idempotency keys will be supported:

Request Header:

X-Idempotency-Key: uuid

Implementation Strategy:

  1. Store idempotency keys with TTL (1 hour)
  2. For duplicate requests, return the same response as the first request
  3. Use Redis for distributed idempotency storage

Example gRPC Method Definition (Protocol Buffer)

syntax = "proto3";

package middleware;

service PipelineService {
  rpc CreatePipeline(CreatePipelineRequest) returns (CreatePipelineResponse);
  rpc GetPipeline(GetPipelineRequest) returns (GetPipelineResponse);
  rpc UpdatePipeline(UpdatePipelineRequest) returns (UpdatePipelineResponse);
  rpc DeletePipeline(DeletePipelineRequest) returns (DeletePipelineResponse);
  rpc ListPipelines(ListPipelinesRequest) returns (ListPipelinesResponse);
  rpc StartPipeline(StartPipelineRequest) returns (StartPipelineResponse);
  rpc StopPipeline(StopPipelineRequest) returns (StopPipelineResponse);
  rpc GetPipelineStatus(GetPipelineStatusRequest) returns (GetPipelineStatusResponse);
  rpc GetPipelineMetrics(GetPipelineMetricsRequest) returns (GetPipelineMetricsResponse);
  rpc ValidatePipeline(ValidatePipelineRequest) returns (ValidatePipelineResponse);
}

message CreatePipelineRequest {
  string name = 1;
  string description = 2;
  repeated PipelineStage stages = 3;
  ErrorHandling error_handling = 4;
  string input_topic = 5;
  string output_topic = 6;
}

message PipelineStage {
  string name = 1;
  StageType type = 2;
  map<string, string> config = 3;
  int32 timeout_ms = 4;
  RetryPolicy retry_policy = 5;
}

enum StageType {
  TRANSFORM = 0;
  FILTER = 1;
  ROUTE = 2;
  LOG = 3;
}

message RetryPolicy {
  int32 max_retries = 1;
  double backoff_multiplier = 2;
  int32 initial_delay_ms = 3;
}

message ErrorHandling {
  ErrorStrategy strategy = 1;
  string fallback_stage = 2;
}

enum ErrorStrategy {
  FAIL = 0;
  CONTINUE = 1;
  SKIP = 2;
}

message CreatePipelineResponse {
  string id = 1;
  string name = 2;
  PipelineStatus status = 3;
  google.protobuf.Timestamp created_at = 4;
  google.protobuf.Timestamp updated_at = 5;
}

enum PipelineStatus {
  CREATED = 0;
  ACTIVE = 1;
  INACTIVE = 2;
  ERROR = 3;
  PAUSED = 4;
}

3. Data Model Design

Entity-Relationship Diagram

erDiagram
    MIDDLEWARE_PIPELINE {
        string id PK
        string name
        string description
        string status
        timestamp created_at
        timestamp updated_at
        string owner_id
        boolean is_active
    }

    MIDDLEWARE_PIPELINE_CONFIG {
        string id PK
        string pipeline_id FK
        string config_key
        string config_value
        string config_type
        timestamp created_at
        timestamp updated_at
    }

    MIDDLEWARE_PIPELINE_STAGE {
        string id PK
        string pipeline_id FK
        integer stage_order
        string stage_type
        string handler_class
        string config_json
        timestamp created_at
        timestamp updated_at
    }

    MIDDLEWARE_PIPELINE_LOG {
        string id PK
        string pipeline_id FK
        string stage_id FK
        string log_level
        string message
        timestamp timestamp
        string error_details
    }

    MIDDLEWARE_PIPELINE_METRIC {
        string id PK
        string pipeline_id FK
        string stage_id FK
        string metric_name
        double value
        timestamp recorded_at
        string unit
    }

    MIDDLEWARE_PIPELINE_CONFIG ||--o{ MIDDLEWARE_PIPELINE : "has"
    MIDDLEWARE_PIPELINE_STAGE ||--o{ MIDDLEWARE_PIPELINE : "belongs_to"
    MIDDLEWARE_PIPELINE_LOG ||--o{ MIDDLEWARE_PIPELINE : "logs"
    MIDDLEWARE_PIPELINE_METRIC ||--o{ MIDDLEWARE_PIPELINE : "metrics"
    MIDDLEWARE_PIPELINE_STAGE ||--o{ MIDDLEWARE_PIPELINE_LOG : "generates"
    MIDDLEWARE_PIPELINE_STAGE ||--o{ MIDDLEWARE_PIPELINE_METRIC : "generates"

Table Schema Definitions

middleware_pipeline Table

CREATE TABLE middleware_pipeline (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
    owner_id UUID NOT NULL,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_pipeline_owner (owner_id),
    INDEX idx_pipeline_status (status),
    INDEX idx_pipeline_active (is_active)
);

middleware_pipeline_config Table

CREATE TABLE middleware_pipeline_config (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    pipeline_id UUID NOT NULL REFERENCES middleware_pipeline(id) ON DELETE CASCADE,
    config_key VARCHAR(255) NOT NULL,
    config_value TEXT,
    config_type VARCHAR(50) NOT NULL DEFAULT 'STRING',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_config_pipeline (pipeline_id),
    INDEX idx_config_key (config_key),
    UNIQUE (pipeline_id, config_key)
);

middleware_pipeline_stage Table

CREATE TABLE middleware_pipeline_stage (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    pipeline_id UUID NOT NULL REFERENCES middleware_pipeline(id) ON DELETE CASCADE,
    stage_order INTEGER NOT NULL,
    stage_type VARCHAR(100) NOT NULL,
    handler_class VARCHAR(255),
    config_json JSONB DEFAULT '{}',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_stage_pipeline (pipeline_id),
    INDEX idx_stage_order (stage_order),
    INDEX idx_stage_type (stage_type),
    UNIQUE (pipeline_id, stage_order)
);

middleware_pipeline_log Table

CREATE TABLE middleware_pipeline_log (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    pipeline_id UUID NOT NULL REFERENCES middleware_pipeline(id) ON DELETE CASCADE,
    stage_id UUID REFERENCES middleware_pipeline_stage(id) ON DELETE SET NULL,
    log_level VARCHAR(20) NOT NULL DEFAULT 'INFO',
    message TEXT NOT NULL,
    timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    error_details JSONB,
    
    INDEX idx_log_pipeline (pipeline_id),
    INDEX idx_log_stage (stage_id),
    INDEX idx_log_timestamp (timestamp),
    INDEX idx_log_level (log_level)
);

middleware_pipeline_metric Table

CREATE TABLE middleware_pipeline_metric (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    pipeline_id UUID NOT NULL REFERENCES middleware_pipeline(id) ON DELETE CASCADE,
    stage_id UUID REFERENCES middleware_pipeline_stage(id) ON DELETE SET NULL,
    metric_name VARCHAR(255) NOT NULL,
    value DOUBLE PRECISION NOT NULL,
    recorded_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    unit VARCHAR(50),
    
    INDEX idx_metric_pipeline (pipeline_id),
    INDEX idx_metric_stage (stage_id),
    INDEX idx_metric_name (metric_name),
    INDEX idx_metric_recorded (recorded_at)
);

Primary Key Strategy

UUIDs are used as primary keys for the following reasons:

  • Distributed system compatibility: UUIDs can be generated without coordination across services
  • Scalability: No need to maintain global sequence counters in distributed environments
  • Security: UUIDs don't reveal information about the creation order or system
  • No hotspots: Avoids database index fragmentation issues in sharded systems

Index Design Justification

  1. Composite Indexes:

    • pipeline_id, stage_order on middleware_pipeline_stage for efficient pipeline traversal
    • pipeline_id, config_key on middleware_pipeline_config for fast configuration lookups
  2. Partial Indexes:

    • WHERE is_active = TRUE on middleware_pipeline to optimize active pipeline queries
    • WHERE log_level IN ('ERROR', 'FATAL') on middleware_pipeline_log for error monitoring
  3. Covering Indexes:

    • pipeline_id, status, is_active on middleware_pipeline to support read-heavy pipeline status queries

Data Partitioning / Sharding Strategy

Sharding Key: owner_id (user/tenant ID)
Strategy: Hash-based sharding with consistent hashing to ensure even distribution and minimal rebalancing during scaling.

-- Example sharding approach (conceptual)
CREATE TABLE middleware_pipeline_shard_0 (...) PARTITION BY HASH (owner_id);
CREATE TABLE middleware_pipeline_shard_1 (...) PARTITION BY HASH (owner_id);
-- ... additional shards

Soft Delete vs. Hard Delete

Soft Delete: Implemented with is_active boolean flag on middleware_pipeline table.
Justification:

  • Enables audit trails and recovery of deleted pipelines
  • Supports soft-deletion for compliance scenarios (e.g., GDPR)
  • Allows rollback of accidental deletions without complex data recovery

Audit Trail Design

Fields:

  • created_at, updated_at timestamps
  • owner_id for ownership tracking
  • status field to track lifecycle changes

Implementation:

  • All mutations are logged in the audit trail via triggers or application-level logging
  • For compliance, audit entries can be stored in a separate audit_log table with full payload snapshots

Data Archival Strategy and Retention Policy

Retention Policy:

  • Active pipelines: 30 days of logs and metrics
  • Inactive pipelines: 90 days of logs and metrics
  • Deleted pipelines: 1 year retention for compliance

Archival Process:

  • Daily batch job to move logs and metrics older than retention period to cold storage (e.g., S3)
  • Metadata is retained in database but actual log content is moved to object storage
  • Indexes and metadata are updated to reflect archival status
// Example archiving logic (Java)
public class PipelineArchiver {
    private final PipelineRepository pipelineRepo;
    private final LogStorage logStorage;

    public void archiveOldPipelines() {
        // Find pipelines older than retention period
        List<Pipeline> oldPipelines = pipelineRepo.findPipelinesOlderThan(30, TimeUnit.DAYS);
        
        for (Pipeline pipeline : oldPipelines) {
            // Archive logs and metrics to object storage
            logStorage.archiveLogs(pipeline.getId());
            logStorage.archiveMetrics(pipeline.getId());
            
            // Update pipeline status to archived
            pipeline.setArchived(true);
            pipelineRepo.save(pipeline);
        }
    }
}

4. Class Design & Service Layer

classDiagram
    class MiddlewarePipeline {
        +execute(Context context)
        +addMiddleware(Middleware middleware)
        +removeMiddleware(String id)
    }
    
    class Middleware {
        <<interface>>
        +handle(Context context, Next next)
    }
    
    class Context {
        +Map~String, Object~ attributes
        +Object getAttribute(String key)
        +void setAttribute(String key, Object value)
        +String getId()
    }
    
    class Next {
        <<interface>>
        +void proceed(Context context)
    }
    
    class PipelineBuilder {
        +build()
    }
    
    class LoggingMiddleware {
        +handle(Context context, Next next)
    }
    
    class AuthenticationMiddleware {
        +handle(Context context, Next next)
    }
    
    class AuthorizationMiddleware {
        +handle(Context context, Next next)
    }
    
    class ErrorHandlingMiddleware {
        +handle(Context context, Next next)
    }
    
    MiddlewarePipeline --> Middleware : contains
    MiddlewarePipeline --> PipelineBuilder : uses
    Context --> Next : passes to
    
    LoggingMiddleware --|> Middleware
    AuthenticationMiddleware --|> Middleware
    AuthorizationMiddleware --|> Middleware
    ErrorHandlingMiddleware --|> Middleware
stateDiagram-v2
    [*] --> Idle
    Idle --> Processing : execute()
    Processing --> Success : success
    Processing --> Error : error
    Error --> [*] : complete
    Success --> [*] : complete

Service Class Structure and Responsibilities

The middleware pipeline system consists of several key components:

  1. MiddlewarePipeline: The core orchestrator that manages middleware execution order and context propagation
  2. Middleware: Interface defining the contract for middleware components to implement
  3. Context: Container for request/response data and metadata that flows through the pipeline
  4. Next: Callback interface to continue pipeline execution to the next middleware
  5. PipelineBuilder: Fluent builder for constructing middleware pipelines with specific configurations

Interface / Abstract Class Design

public interface Middleware {
    void handle(Context context, Next next);
}

public interface Next {
    void proceed(Context context);
}

public class Context {
    private final String id;
    private final Map<String, Object> attributes = new HashMap<>();
    
    public Context(String id) {
        this.id = id;
    }
    
    public String getId() {
        return id;
    }
    
    public Object getAttribute(String key) {
        return attributes.get(key);
    }
    
    public void setAttribute(String key, Object value) {
        attributes.put(key, value);
    }
    
    public Map<String, Object> getAttributes() {
        return new HashMap<>(attributes);
    }
}

Business Logic Encapsulation Patterns

The system implements the Chain of Responsibility design pattern to allow flexible middleware composition and execution flow. Each middleware component can choose whether to continue the chain or terminate processing.

Critical-Path Method Implementation

public class MiddlewarePipeline {
    private final List<Middleware> middlewares = new ArrayList<>();
    
    public void execute(Context context) {
        if (middlewares.isEmpty()) {
            return;
        }
        
        // Start the chain with the first middleware
        executeNext(context, 0);
    }
    
    private void executeNext(Context context, int index) {
        if (index >= middlewares.size()) {
            return;
        }
        
        Middleware current = middlewares.get(index);
        Next next = nextContext -> executeNext(nextContext, index + 1);
        
        try {
            current.handle(context, next);
        } catch (Exception e) {
            // Handle exception in error middleware or rethrow
            throw new MiddlewareExecutionException("Error in middleware execution", e);
        }
    }
    
    public void addMiddleware(Middleware middleware) {
        middlewares.add(middleware);
    }
    
    public void removeMiddleware(String id) {
        // Implementation depends on how middleware is identified
        // This could be based on class name, custom ID field, or other metadata
    }
}

Transaction Boundary Design

The middleware pipeline operates as a single logical unit of work. Each execution of the pipeline represents one complete transaction boundary, where all middleware components execute in sequence within a consistent context.

Service Interface Contracts

public interface MiddlewarePipelineService {
    void execute(Context context);
    void addMiddleware(Middleware middleware);
    void removeMiddleware(String id);
}

Saga / Orchestration Pattern

For complex scenarios involving distributed operations, the middleware pipeline can be extended to support saga orchestration patterns where specific middleware components handle coordination of distributed transactions.

Domain Object / Value Object Design

public class MiddlewareConfig {
    private final String id;
    private final String name;
    private final int priority;
    private final boolean enabled;
    
    public MiddlewareConfig(String id, String name, int priority, boolean enabled) {
        this.id = id;
        this.name = name;
        this.priority = priority;
        this.enabled = enabled;
    }
    
    // Getters and equals/hashCode methods
}

public class PipelineConfiguration {
    private final List<MiddlewareConfig> middlewareConfigs;
    private final String name;
    
    public PipelineConfiguration(String name, List<MiddlewareConfig> middlewareConfigs) {
        this.name = name;
        this.middlewareConfigs = new ArrayList<>(middlewareConfigs);
    }
    
    // Getters and equals/hashCode methods
}

Example Middleware Implementations

public class LoggingMiddleware implements Middleware {
    private final Logger logger = LoggerFactory.getLogger(LoggingMiddleware.class);
    
    @Override
    public void handle(Context context, Next next) {
        long startTime = System.currentTimeMillis();
        logger.info("Starting middleware execution for context: {}", context.getId());
        
        try {
            next.proceed(context);
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            logger.info("Completed middleware execution for context: {} in {}ms", 
                       context.getId(), duration);
        }
    }
}

public class AuthenticationMiddleware implements Middleware {
    private final AuthenticationService authService;
    
    public AuthenticationMiddleware(AuthenticationService authService) {
        this.authService = authService;
    }
    
    @Override
    public void handle(Context context, Next next) {
        String token = (String) context.getAttribute("auth-token");
        if (token == null || !authService.validateToken(token)) {
            throw new AuthenticationException("Invalid or missing authentication token");
        }
        
        // Set authenticated user in context
        String userId = authService.getUserId(token);
        context.setAttribute("user-id", userId);
        
        next.proceed(context);
    }
}

public class AuthorizationMiddleware implements Middleware {
    private final AuthorizationService authzService;
    
    public AuthorizationMiddleware(AuthorizationService authzService) {
        this.authzService = authzService;
    }
    
    @Override
    public void handle(Context context, Next next) {
        String userId = (String) context.getAttribute("user-id");
        String resource = (String) context.getAttribute("resource");
        
        if (!authzService.hasPermission(userId, resource)) {
            throw new AuthorizationException("User does not have permission to access resource");
        }
        
        next.proceed(context);
    }
}

5. Concurrency & Thread Safety

Concurrency Model

The middleware pipeline will operate in a multi-threaded environment using Java's java.util.concurrent framework. Each middleware component will execute in its own thread or thread pool, allowing for concurrent processing of multiple requests. The pipeline execution model will leverage a reactive programming approach using Project Reactor or RxJava to handle asynchronous request processing and backpressure.

Race Condition Prevention Strategy

Race conditions in the middleware pipeline will be mitigated through:

  1. Immutable state objects - All middleware configurations and request context objects will be immutable to prevent concurrent modification
  2. Thread-local storage - Per-request state will be stored in thread-local variables to avoid cross-contamination
  3. Synchronized access - Critical shared resources (e.g., global configuration, metrics counters) will use synchronized blocks or atomic references
  4. Lock-free data structures - For high-performance scenarios, concurrent collections like ConcurrentHashMap and AtomicReference will be used

Optimistic vs. Pessimistic Locking Selection

Optimistic locking will be used for middleware state management because:

  • Middleware components typically process independent requests
  • The likelihood of concurrent conflicts is low
  • Performance is prioritized over strict consistency guarantees
  • Middleware state is primarily read-only during processing

Deadlock Prevention Strategy

To prevent deadlocks in the middleware pipeline:

  1. Fixed ordering of resource acquisition - All middleware components will acquire locks in a predefined order (e.g., by component name or priority)
  2. Timeout-based lock acquisition - All lock acquisitions will have a timeout (500ms) to prevent indefinite blocking
  3. Avoid nested locks - Middleware components will not hold multiple locks simultaneously
  4. Thread pool size limiting - Controlled thread pool sizes to prevent resource exhaustion

Idempotency Implementation for Concurrent Duplicate Requests

Idempotency will be handled at the middleware pipeline level:

public class MiddlewarePipeline {
    private final Map<String, AtomicBoolean> idempotencyCache = new ConcurrentHashMap<>();
    
    public CompletableFuture<MiddlewareResult> processRequest(
            MiddlewareRequest request, 
            String idempotencyKey) {
        
        // Check if this exact request is already being processed
        AtomicBoolean processing = idempotencyCache.computeIfAbsent(
            idempotencyKey, k -> new AtomicBoolean(false));
        
        if (processing.getAndSet(true)) {
            // Return cached result or wait for completion
            return waitForCompletion(idempotencyKey);
        }
        
        try {
            return executePipeline(request, idempotencyKey);
        } finally {
            processing.set(false);
            idempotencyCache.remove(idempotencyKey);
        }
    }
}

The middleware pipeline will support idempotency keys for:

  • Request deduplication - Identical requests are not reprocessed
  • Retry handling - Idempotent middleware components can safely retry without side effects
  • State consistency - Prevents duplicate processing in distributed environments

Thread Safety Patterns

  1. Immutable Configuration Objects - All middleware configurations will be immutable to ensure thread safety
  2. Thread-Safe Component Registration - Middleware component registration will use concurrent collections and synchronized initialization
  3. Atomic State Transitions - Pipeline state changes will be atomic using AtomicReference or similar constructs
  4. Synchronized Metrics Collection - Performance metrics will be collected using thread-safe counters and histograms

Concurrency Control in Pipeline Execution

The pipeline execution will use a worker thread pool with configurable size:

public class MiddlewarePipeline {
    private final ExecutorService workerPool;
    private final Semaphore concurrentRequestSemaphore;
    
    public MiddlewarePipeline(int maxConcurrentRequests) {
        this.workerPool = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors() * 2);
        this.concurrentRequestSemaphore = new Semaphore(maxConcurrentRequests);
    }
    
    public CompletableFuture<MiddlewareResult> processRequest(MiddlewareRequest request) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                concurrentRequestSemaphore.acquire();
                return executePipeline(request);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Request processing interrupted", e);
            } finally {
                concurrentRequestSemaphore.release();
            }
        }, workerPool);
    }
}

This approach ensures:

  • Controlled concurrency - Maximum concurrent requests are bounded
  • Non-blocking I/O - Asynchronous processing prevents thread blocking
  • Resource isolation - Each request gets dedicated processing resources
  • Scalability - Pipeline can scale with available system resources

6. Sequence Diagrams for Key Flows

sequenceDiagram
    participant Client
    participant MiddlewarePipeline
    participant Handler1
    participant Handler2
    participant Handler3
    participant Service

    Client->>MiddlewarePipeline: Request
    MiddlewarePipeline->>Handler1: Invoke Handler1
    Handler1->>Handler2: Invoke Handler2
    Handler2->>Handler3: Invoke Handler3
    Handler3->>Service: Invoke Service
    Service-->>Handler3: Response
    Handler3-->>Handler2: Response
    Handler2-->>Handler1: Response
    Handler1-->>MiddlewarePipeline: Response
    MiddlewarePipeline-->>Client: Final Response
sequenceDiagram
    participant Client
    participant MiddlewarePipeline
    participant Handler1
    participant Handler2
    participant ErrorHandler

    Client->>MiddlewarePipeline: Request
    MiddlewarePipeline->>Handler1: Invoke Handler1
    Handler1->>Handler2: Invoke Handler2
    Handler2-->>Handler1: Error
    Handler1->>ErrorHandler: Handle Error
    ErrorHandler-->>MiddlewarePipeline: Error Response
    MiddlewarePipeline-->>Client: Error Response
sequenceDiagram
    participant Client
    participant MiddlewarePipeline
    participant AsyncHandler
    participant MessageBroker
    participant Consumer

    Client->>MiddlewarePipeline: Request
    MiddlewarePipeline->>AsyncHandler: Invoke Async Handler
    AsyncHandler->>MessageBroker: Publish Event
    MessageBroker-->>Consumer: Deliver Event
    Consumer->>Service: Process Event
    Service-->>Consumer: Acknowledge
    Consumer-->>MessageBroker: Acknowledge

7. Caching Design

Cache Strategy Selection

For middleware pipeline, we'll use cache-aside pattern with a dual-layer caching strategy:

  • L1 Cache: In-memory (Caffeine) for high-frequency, low-latency access
  • L2 Cache: Distributed Redis cluster for persistence and cross-service sharing

This approach provides optimal performance for frequently accessed middleware configurations while maintaining consistency across service instances.

Cache Key Design

Cache keys will follow a structured namespace pattern:

{cacheNamespace}:{entityType}:{entityId}:{version}:{context}

Example key structure:

middleware:pipeline:12345:v1:auth
middleware:pipeline:12345:v1:rate_limiting

TTL Strategy

Data TypeTTLReason
Pipeline Configurations5 minutesFrequent updates during deployment
Middleware Rules1 hourStable once deployed
Auth Tokens30 minutesSession timeout requirements
Rate Limiting Buckets1 minutePer-second granularity needed
User Context15 minutesRecent user activity

Cache Eviction Policy

Primary policy: LRU (Least Recently Used) with TTL fallback

  • LRU ensures hot paths remain cached
  • TTL prevents stale data from persisting indefinitely
  • Cache warming strategy for critical pipelines during boot

Cache Stampede Prevention

Probabilistic early expiry with distributed locks:

public class CacheStampedePrevention {
    private final RedisTemplate<String, String> redisTemplate;
    
    public String getWithStampedeProtection(String cacheKey, 
                                          Supplier<String> dataLoader,
                                          int ttlSeconds) {
        String cachedValue = redisTemplate.opsForValue().get(cacheKey);
        if (cachedValue != null) {
            return cachedValue;
        }
        
        // Try to acquire lock for cache population
        String lockKey = cacheKey + ":lock";
        String lockValue = UUID.randomUUID().toString();
        
        if (redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 
                                                   Duration.ofSeconds(10))) {
            try {
                // Double-check pattern
                cachedValue = redisTemplate.opsForValue().get(cacheKey);
                if (cachedValue == null) {
                    // Load fresh data
                    cachedValue = dataLoader.get();
                    redisTemplate.opsForValue().set(cacheKey, cachedValue, 
                                                   Duration.ofSeconds(ttlSeconds));
                }
            } finally {
                // Release lock
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                               "return redis.call('del', KEYS[1]) else return 0 end";
                redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                                    Collections.singletonList(lockKey), lockValue);
            }
        } else {
            // Wait briefly and retry
            Thread.sleep(50);
            return redisTemplate.opsForValue().get(cacheKey);
        }
        
        return cachedValue;
    }
}

Distributed Cache Architecture

Redis Cluster Configuration:

  • 3 master nodes with 3 replicas each (6 total instances)
  • Automatic failover with Redis Sentinel
  • Sharding based on pipeline ID hash for even distribution
  • Memory policy: allkeys-lru with 2GB max memory per node

Cache Invalidation Strategy

Event-driven invalidation with cache tags:

public class MiddlewareCacheInvalidator {
    private final RedisTemplate<String, String> redisTemplate;
    
    public void invalidatePipelineCache(String pipelineId) {
        // Tag-based invalidation for efficient cleanup
        String tagKey = "pipeline:" + pipelineId + ":tags";
        Set<String> tags = redisTemplate.opsForSet().members(tagKey);
        
        for (String tag : tags) {
            String cacheKeyPattern = "middleware:" + tag + ":*";
            Set<String> keysToDelete = redisTemplate.keys(cacheKeyPattern);
            redisTemplate.delete(keysToDelete);
        }
        
        // Also invalidate specific pipeline cache
        String specificKey = "middleware:pipeline:" + pipelineId + ":*";
        Set<String> keysToDelete = redisTemplate.keys(specificKey);
        redisTemplate.delete(keysToDelete);
    }
}

Cache Performance Considerations

  1. Warm-up strategy: Preload critical pipeline configurations during service startup
  2. Cache hit ratio monitoring: Track and alert on sub-50% hit ratios
  3. Memory usage tracking: Monitor L1 and Redis memory consumption
  4. Cache consistency: Event-driven invalidation ensures all instances stay in sync

Cache Read-Through Pattern

public class MiddlewareCache {
    private final LoadingCache<String, MiddlewarePipeline> l1Cache;
    private final RedisTemplate<String, String> redisTemplate;
    
    public MiddlewarePipeline getPipeline(String pipelineId) {
        // First check L1 cache
        MiddlewarePipeline pipeline = l1Cache.getIfPresent(pipelineId);
        if (pipeline != null) {
            return pipeline;
        }
        
        // Then check Redis cache
        String redisKey = "middleware:pipeline:" + pipelineId;
        String cachedJson = redisTemplate.opsForValue().get(redisKey);
        
        if (cachedJson != null) {
            MiddlewarePipeline pipelineFromRedis = parseFromJson(cachedJson);
            l1Cache.put(pipelineId, pipelineFromRedis); // Populate L1
            return pipelineFromRedis;
        }
        
        // Cache miss - load from database and populate caches
        MiddlewarePipeline pipelineFromDB = loadPipelineFromDB(pipelineId);
        String jsonValue = serializeToJson(pipelineFromDB);
        
        // Write to both caches
        l1Cache.put(pipelineId, pipelineFromDB);
        redisTemplate.opsForValue().set(redisKey, jsonValue, 
                                       Duration.ofMinutes(5));
        
        return pipelineFromDB;
    }
}

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 logic and database operations. Each entity has its own repository interface with standardized methods for CRUD operations.

public interface UserRepository {
    User findById(UUID id);
    User findByEmail(String email);
    List<User> findAll();
    User save(User user);
    void delete(UUID id);
    boolean existsById(UUID id);
}

public interface UserRepositoryImpl implements UserRepository {
    private final JdbcTemplate jdbcTemplate;
    
    @Override
    public User findById(UUID id) {
        return jdbcTemplate.queryForObject(
            "SELECT * FROM users WHERE id = ?", 
            new UserRowMapper(), 
            id
        );
    }
    
    @Override
    public User findByEmail(String email) {
        return jdbcTemplate.queryForObject(
            "SELECT * FROM users WHERE email = ?", 
            new UserRowMapper(), 
            email
        );
    }
    
    @Override
    public List<User> findAll() {
        return jdbcTemplate.query(
            "SELECT * FROM users ORDER BY created_at DESC", 
            new UserRowMapper()
        );
    }
    
    @Override
    public User save(User user) {
        if (user.getId() == null) {
            return create(user);
        } else {
            return update(user);
        }
    }
    
    private User create(User user) {
        String sql = "INSERT INTO users (id, email, name, created_at, updated_at) VALUES (?, ?, ?, ?, ?)";
        jdbcTemplate.update(sql, user.getId(), user.getEmail(), user.getName(), 
                           user.getCreatedAt(), user.getUpdatedAt());
        return user;
    }
    
    private User update(User user) {
        String sql = "UPDATE users SET email = ?, name = ?, updated_at = ? WHERE id = ?";
        jdbcTemplate.update(sql, user.getEmail(), user.getName(), 
                           user.getUpdatedAt(), user.getId());
        return user;
    }
    
    @Override
    public void delete(UUID id) {
        jdbcTemplate.update("DELETE FROM users WHERE id = ?", id);
    }
    
    @Override
    public boolean existsById(UUID id) {
        return jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM users WHERE id = ?", 
            Integer.class, 
            id
        ) > 0;
    }
}

ORM vs. Raw Query Decisions

For this middleware pipeline, we use raw SQL queries instead of an ORM for better performance and control over database interactions. The decision is based on:

  1. Performance: Raw queries eliminate ORM overhead and allow for fine-tuned optimization
  2. Control: Direct query control is needed for complex middleware logic and filtering
  3. Simplicity: Middleware operations are typically straightforward CRUD, making ORM complexity unnecessary

Read vs. Write Separation

The middleware pipeline follows a read-write separation pattern to handle different data access patterns efficiently:

// Read-optimized repository for middleware configuration queries
public interface MiddlewareConfigReadRepository {
    List<MiddlewareConfig> findAllActive();
    MiddlewareConfig findByKey(String key);
    List<MiddlewareConfig> findByType(MiddlewareType type);
}

// Write-optimized repository for configuration updates
public interface MiddlewareConfigWriteRepository {
    MiddlewareConfig save(MiddlewareConfig config);
    void delete(String key);
    int updateStatus(String key, ConfigStatus status);
}

Connection Pool Configuration

Connection pooling is configured with the following parameters to handle concurrent middleware pipeline operations:

# application.yml
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      validation-timeout: 5000
      leak-detection-threshold: 60000

N+1 Query Prevention Strategy

To prevent N+1 query problems in middleware pipeline operations:

// Using eager loading with JOINs for related data
public List<MiddlewarePipeline> findAllWithConfigurations() {
    String sql = """
        SELECT p.id, p.name, p.created_at, 
               c.key as config_key, c.value as config_value
        FROM middleware_pipelines p
        LEFT JOIN middleware_configurations c ON p.id = c.pipeline_id
        ORDER BY p.created_at DESC
        """;
    
    return jdbcTemplate.query(sql, new PipelineWithConfigRowMapper());
}

// Using batch operations for bulk updates
public void updateMultipleConfigs(List<MiddlewareConfig> configs) {
    String sql = "UPDATE middleware_configurations SET value = ?, updated_at = ? WHERE key = ?";
    
    List<Object[]> batchArgs = configs.stream()
        .map(config -> new Object[]{config.getValue(), config.getUpdatedAt(), config.getKey()})
        .collect(Collectors.toList());
    
    jdbcTemplate.batchUpdate(sql, batchArgs);
}

Batch Operation Design

Batch operations are implemented for efficient middleware configuration updates:

public class MiddlewareConfigBatchProcessor {
    private final JdbcTemplate jdbcTemplate;
    
    public void batchUpdateConfigs(List<MiddlewareConfig> configs) {
        String sql = "UPDATE middleware_configurations SET value = ?, updated_at = ? WHERE key = ?";
        
        List<Object[]> batchArgs = configs.stream()
            .map(config -> new Object[]{config.getValue(), config.getUpdatedAt(), config.getKey()})
            .collect(Collectors.toList());
            
        jdbcTemplate.batchUpdate(sql, batchArgs);
    }
    
    public void batchUpsertConfigs(List<MiddlewareConfig> configs) {
        String upsertSql = """
            INSERT INTO middleware_configurations (key, value, pipeline_id, created_at, updated_at) 
            VALUES (?, ?, ?, ?, ?)
            ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at
            """;
            
        List<Object[]> batchArgs = configs.stream()
            .map(config -> new Object[]{config.getKey(), config.getValue(), 
                                       config.getPipelineId(), config.getCreatedAt(), config.getUpdatedAt()})
            .collect(Collectors.toList());
            
        jdbcTemplate.batchUpdate(upsertSql, batchArgs);
    }
}

Transaction Management

Transaction boundaries are carefully defined for middleware operations:

@Service
@Transactional
public class MiddlewarePipelineService {
    private final MiddlewareConfigRepository configRepo;
    
    public void updatePipelineWithConfigs(UUID pipelineId, List<MiddlewareConfig> configs) {
        // All operations within this method are part of the same transaction
        middlewarePipelineRepo.updateStatus(pipelineId, PipelineStatus.ACTIVE);
        
        // Batch update configs
        configRepo.batchUpdateConfigs(configs);
        
        // Log the operation
        auditLogService.logPipelineUpdate(pipelineId, configs.size());
    }
}

Repository Interface Contracts

All repository interfaces follow a consistent contract pattern:

public interface BaseRepository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    T save(T entity);
    void delete(ID id);
    boolean existsById(ID id);
    
    // Additional query methods specific to the entity type
    List<T> findByCriteria(Criteria criteria);
}

Data Access Layer Design Principles

  1. Single Responsibility: Each repository handles one domain entity type
  2. Consistent API: All repositories implement the same interface contract
  3. Performance Focus: Raw SQL queries with proper indexing and batching
  4. Transaction Safety: Clear transaction boundaries for data consistency
  5. Scalability: Read-write separation and batch operations for high-volume scenarios

9. Asynchronous Processing & Messaging

Event-driven boundaries in this feature — what triggers async vs. what stays synchronous, and why

The middleware pipeline will process incoming HTTP requests synchronously for immediate response to the client. However, certain operations within the pipeline will be offloaded to asynchronous processing:

  1. Request logging and audit trail - Writing detailed request/response logs to external systems (e.g., ELK stack or data lake) should be async to avoid blocking the HTTP response.
  2. Metrics and telemetry - Publishing metrics to monitoring systems (e.g., Prometheus, Datadog) should be async to avoid impacting request latency.
  3. External service notifications - Sending events to external systems (e.g., payment gateway, notification service) should be async to prevent request timeouts.
  4. Data archival and backup - Archiving old pipeline configurations or logs should be async to avoid blocking the main processing path.

These operations are decoupled from the primary request flow to ensure that failures in external systems don't impact the core request processing and to maintain high throughput.

Message schema design with a sample payload (Avro / Protobuf / JSON Schema — with versioning strategy)

{
  "type": "object",
  "title": "MiddlewareEvent",
  "version": "1.0.0",
  "properties": {
    "eventId": {
      "type": "string",
      "description": "Unique identifier for the event"
    },
    "timestamp": {
      "type": "string",
      "format": "date-time",
      "description": "ISO 8601 timestamp of event creation"
    },
    "eventType": {
      "type": "string",
      "enum": ["REQUEST_LOG", "METRICS_PUBLISH", "EXTERNAL_NOTIFICATION", "ARCHIVE_DATA"],
      "description": "Type of middleware event"
    },
    "payload": {
      "type": "object",
      "description": "Event-specific payload"
    },
    "correlationId": {
      "type": "string",
      "description": "Correlation ID to track request flow"
    }
  },
  "required": ["eventId", "timestamp", "eventType", "payload"]
}

Versioning strategy:

  • Use semantic versioning (MAJOR.MINOR.PATCH) for message schemas.
  • Implement schema registry to maintain backward compatibility.
  • Support schema evolution with forward and backward compatibility checks.

Dead letter queue strategy and poison message handling

Poison messages (messages that consistently fail processing) will be routed to a dead letter queue (DLQ) after a configurable number of retries.

DLQ Strategy:

  1. Messages that fail more than 3 times will be moved to DLQ.
  2. DLQ messages will be stored in a dedicated topic with a retention policy of 7 days.
  3. DLQ messages will be tagged with metadata indicating the original topic, error type, and failure count.

Poison Message Handling:

  1. DLQ messages will be flagged for manual inspection by operations team.
  2. A separate monitoring dashboard will surface DLQ messages with high failure rates.
  3. Failed messages in DLQ will trigger alerts to on-call engineers.

At-least-once vs. exactly-once delivery — decision with justification for this feature

Decision: At-least-once delivery

Justification:

  • Middleware events are primarily used for logging, metrics, and notifications.
  • The system is designed to be idempotent where possible (e.g., duplicate logs won't cause harm).
  • The additional complexity of exactly-once delivery (durable transactions, idempotency keys) is not justified for this use case.
  • At-least-once delivery provides sufficient reliability with better performance characteristics.

Retry policy for consumers (max attempts, backoff formula, non-retryable error classification)

public class RetryPolicy {
    private static final int MAX_RETRIES = 3;
    private static final long BASE_DELAY_MS = 1000;
    private static final double BACKOFF_MULTIPLIER = 2.0;
    private static final double JITTER_RANGE = 0.1; // 10% jitter
    
    public static boolean shouldRetry(Exception exception) {
        return !(exception instanceof IllegalArgumentException ||
                exception instanceof ValidationException ||
                exception instanceof UnauthorizedException);
    }
    
    public static long getDelay(int attempt) {
        double exponentialDelay = BASE_DELAY_MS * Math.pow(BACKOFF_MULTIPLIER, attempt);
        double jitter = exponentialDelay * (Math.random() * 2 * JITTER_RANGE - JITTER_RANGE);
        return Math.round(exponentialDelay + jitter);
    }
}

Outbox pattern for reliable event publishing — include or explicitly justify omission

Omission justified:

The middleware pipeline will use a direct asynchronous processing model rather than the outbox pattern for the following reasons:

  1. Simplicity: The middleware pipeline is designed to be lightweight and focused on request processing.
  2. Eventual consistency: The asynchronous events (logging, metrics) are acceptable to lose in edge cases.
  3. Performance: The outbox pattern adds an extra database write and read, which would impact performance for a middleware system.
  4. Existing infrastructure: The system assumes that the message broker (e.g., Kafka) provides reliable delivery guarantees.

Instead, we rely on:

  • Message broker reliability (Kafka's durability features)
  • Retry mechanisms in consumers
  • Dead letter queue handling for failed messages

Sequence diagram for all async flows (Mermaid)

sequenceDiagram
    participant Client
    participant Middleware
    participant MessageBroker
    participant ExternalService
    
    Client->>Middleware: HTTP Request
    Middleware->>Middleware: Process request synchronously
    Middleware->>MessageBroker: Publish to async topic (e.g., "middleware-logs")
    MessageBroker->>Middleware: Acknowledge
    Middleware->>Client: Return HTTP response
    
    loop Async Processing
        MessageBroker->>ExternalService: Deliver event (e.g., log data)
        ExternalService-->>MessageBroker: Acknowledge or Nack
        alt Retry logic
            MessageBroker->>ExternalService: Retry with exponential backoff
            ExternalService-->>MessageBroker: Acknowledge or move to DLQ
        end
    end
    
    MessageBroker->>Middleware: Handle DLQ messages
    Middleware->>Middleware: Alert operations team on DLQ

10. Error Handling Design

Error Classification Hierarchy

The middleware pipeline implements a three-tier error classification system:

public enum ErrorType {
    VALIDATION_ERROR,      // Input validation failures
    BUSINESS_LOGIC_ERROR,  // Domain-specific business rule violations
    INFRASTRUCTURE_ERROR,  // External service failures, DB issues, etc.
    SYSTEM_ERROR           // Internal system errors (unhandled exceptions)
}

Error Propagation Strategy

Errors follow a layered propagation pattern:

  1. Local handling: Specific error types are caught and transformed at service boundaries
  2. Aggregated errors: Multiple errors from pipeline stages are collected and wrapped in a composite error
  3. Global escalation: Unhandled system errors bubble up to the middleware handler for centralized logging and response generation

Retry Strategy with Exponential Backoff and Jitter

For transient infrastructure errors (e.g., network timeouts, external service unavailability):

public class RetryConfig {
    private int maxAttempts = 3;
    private long initialDelayMs = 1000;
    private double backoffMultiplier = 2.0;
    private double jitterPercentage = 0.1; // ±10% jitter
    
    public long getDelay(int attempt) {
        double delay = initialDelayMs * Math.pow(backoffMultiplier, attempt - 1);
        double jitter = delay * (Math.random() * 2 * jitterPercentage - jitterPercentage);
        return Math.round(delay + jitter);
    }
}

Circuit Breaker Design

Used for external service dependencies to prevent cascading failures:

public class CircuitBreaker {
    private final int failureThreshold = 5;
    private final long timeoutMs = 60000; // 1 minute
    private final int successThreshold = 5;
    
    private volatile CircuitState state = CircuitState.CLOSED;
    private int failureCount = 0;
    private long lastFailureTime = 0;
    
    public enum CircuitState {
        CLOSED, OPEN, HALF_OPEN
    }
    
    public boolean canExecute() {
        switch (state) {
            case CLOSED:
                return true;
            case HALF_OPEN:
                // Allow one request to test the service
                return true;
            case OPEN:
                if (System.currentTimeMillis() - lastFailureTime > timeoutMs) {
                    state = CircuitState.HALF_OPEN;
                    return true;
                }
                return false;
        }
        return false;
    }
    
    public void recordSuccess() {
        if (state == CircuitState.HALF_OPEN) {
            failureCount = 0;
            state = CircuitState.CLOSED;
        }
    }
    
    public void recordFailure() {
        lastFailureTime = System.currentTimeMillis();
        failureCount++;
        
        if (failureCount >= failureThreshold) {
            state = CircuitState.OPEN;
        }
    }
}

Fallback Behavior Design

Each middleware stage defines appropriate fallback behavior:

public interface MiddlewareFallback<T> {
    T fallback(T input, Exception error);
}

// Example fallback for rate limiting middleware
public class RateLimitFallback implements MiddlewareFallback<RequestContext> {
    @Override
    public RequestContext fallback(RequestContext input, Exception error) {
        // Return default context with rate limit headers
        return new RequestContext.Builder()
            .from(input)
            .withRateLimitHeaders(defaultRateLimitHeaders())
            .build();
    }
}

Error Logging Format and Alerting Thresholds

Structured logging with consistent format:

public class MiddlewareErrorLogger {
    private static final Logger logger = LoggerFactory.getLogger(MiddlewareErrorLogger.class);
    
    public void logError(String operation, Exception error, Map<String, Object> context) {
        Map<String, Object> logEntry = new HashMap<>();
        logEntry.put("timestamp", Instant.now());
        logEntry.put("operation", operation);
        logEntry.put("error_type", error.getClass().getSimpleName());
        logEntry.put("error_message", error.getMessage());
        logEntry.put("trace_id", MDC.get("trace_id"));
        logEntry.put("span_id", MDC.get("span_id"));
        logEntry.put("context", context);
        
        logger.error("Middleware pipeline error: {}", logEntry);
    }
    
    // Alert thresholds
    private static final int ERROR_ALERT_THRESHOLD = 10; // Per minute
    private static final int SYSTEM_ERROR_ALERT_THRESHOLD = 5; // Per minute
}

Alerting thresholds:

  • Validation errors: Alert if > 100 per minute (high volume of malformed requests)
  • Infrastructure errors: Alert if > 5 per minute (external service outages)
  • System errors: Alert immediately (critical internal failures)
  • Business logic errors: Alert if > 10 per minute (potential data integrity issues)

11. Security Design

Input Validation and Sanitization Strategy

All incoming requests are validated using a combination of:

  • Schema validation using JSON Schema for request bodies
  • Custom validation rules via Apache BVal (Bean Validation) for business logic constraints
  • Input sanitization using OWASP Java HTML Sanitizer for any text fields that may contain user-generated content
public class RequestValidator {
    private static final HtmlSanitizer sanitizer = 
        new HtmlSanitizer.Builder()
            .allowElements("p", "br", "strong", "em")
            .build();
    
    public static String sanitizeInput(String input) {
        return sanitizer.sanitize(input);
    }
}

SQL Injection Prevention

  • Prepared statements exclusively used for all database queries
  • Parameterized queries with no string concatenation in SQL generation
  • Whitelisting of column and table names for dynamic queries
public class SecureQueryService {
    private final Connection connection;
    
    public List<User> getUsersByRole(String role) throws SQLException {
        String sql = "SELECT * FROM users WHERE role = ?";
        try (PreparedStatement stmt = connection.prepareStatement(sql)) {
            stmt.setString(1, role);
            ResultSet rs = stmt.executeQuery();
            // Process results...
        }
    }
}

Authentication Middleware Design

  • JWT-based authentication for API endpoints with refresh token support
  • OAuth2 integration for third-party service authentication
  • API key validation for service-to-service communication
@Component
public class AuthenticationMiddleware {
    private final JwtService jwtService;
    
    public void validateToken(ServerWebExchange exchange) {
        String token = extractToken(exchange);
        if (!jwtService.validate(token)) {
            throw new SecurityException("Invalid or expired token");
        }
    }
    
    private String extractToken(ServerWebExchange exchange) {
        String bearerToken = exchange.getRequest().getHeaders()
            .getFirst(HttpHeaders.AUTHORIZATION);
        return bearerToken != null && bearerToken.startsWith("Bearer ")
            ? bearerToken.substring(7) : null;
    }
}

Authorization Enforcement

  • Role-Based Access Control (RBAC) implemented via Spring Security annotations
  • Attribute-based access control for sensitive operations using custom permission evaluators
@PreAuthorize("hasRole('ADMIN') or @permissionEvaluator.hasPermission(authentication, #userId, 'USER')")
public void updateUser(String userId, UserUpdateRequest request) {
    // Update logic here
}

@Component
public class PermissionEvaluator implements org.springframework.security.access.PermissionEvaluator {
    @Override
    public boolean hasPermission(Authentication auth, Object targetId, Object targetType) {
        // Custom permission logic for user ownership checks
        return true;
    }
}

Sensitive Data Encryption

  • At-rest encryption: AES-256-GCM using AWS KMS for sensitive fields (passwords, PII)
  • In-transit encryption: TLS 1.3 for all API communications
  • Key rotation policy: 90-day automatic key rotation with proper audit trail
@Service
public class DataEncryptionService {
    private final KmsClient kmsClient;
    
    public String encrypt(String plaintext) {
        return kmsClient.encrypt(plaintext, "alias/application-key");
    }
    
    public String decrypt(String ciphertext) {
        return kmsClient.decrypt(ciphertext, "alias/application-key");
    }
}

PII Handling and Masking Strategy

  • PII fields: SSN, email, phone numbers are encrypted at rest
  • Masking in logs: PII fields are masked with *** in logs and observability tools
  • Data retention: 30-day automatic deletion of PII from logs after processing
public class PiiMasker {
    public static String maskEmail(String email) {
        if (email == null || email.isEmpty()) return email;
        String[] parts = email.split("@");
        if (parts.length < 2) return email;
        
        String username = parts[0];
        String domain = parts[1];
        
        return username.length() > 2 
            ? username.substring(0, 2) + "***@" + domain
            : username + "***@" + domain;
    }
}

Secrets Management

  • AWS Secrets Manager for storing database credentials, API keys, and encryption keys
  • Rotation policy: 90-day automatic rotation with proper fallback mechanisms
  • Access control: IAM policies limiting access to secrets to specific services only

Audit Logging Design

  • Events logged: Authentication attempts, authorization failures, sensitive data access
  • Log format: Structured JSON with trace_id, user_id, operation_type, timestamp, and success/failure status
  • Retention policy: 90-day retention with automatic archival to S3 for compliance
{
  "timestamp": "2023-10-15T14:30:45.123Z",
  "trace_id": "abc123def456",
  "user_id": "user-123",
  "operation_type": "API_ACCESS",
  "resource": "/api/users/123",
  "status": "SUCCESS",
  "ip_address": "192.168.1.100"
}

12. Testing Strategy

Unit Testing Scope and Coverage Targets (%)

  • Service Layer: 85% code coverage with 100% branch coverage for business logic paths
  • Repository Layer: 90% coverage with focus on query execution and error handling
  • Domain Objects: 100% coverage for value objects and immutable entities
  • Middleware Pipeline: 95% coverage including all middleware stages and error paths
  • Exception Handling: 100% coverage for all handled exception scenarios

Integration Testing Strategy

  • Database Integration: Use Testcontainers with PostgreSQL 13+ for end-to-end pipeline testing
  • External Service Mocking: WireMock for external API dependencies (e.g., auth, payment)
  • Message Queue Integration: Embedded Kafka or RabbitMQ for async pipeline testing
  • Cross-Service Boundaries: Use embedded service containers to test middleware interaction across services

Contract Testing

  • Pact Broker Integration: Implement consumer-driven contract testing using Pact for external API contracts
  • API Versioning: Test all major version paths with contract verification
  • Event Schema Validation: Use JSON Schema or Avro schema registry for event contract validation

Performance Testing Tool and Key Scenarios

  • Tool: k6 with 500 concurrent users for middleware pipeline stress testing
  • Key Scenarios:
    • Pipeline processing under load (10k req/sec)
    • Middleware latency impact analysis (50th/95th/99th percentile)
    • Memory usage under sustained load (GC pressure, heap allocation)
    • Error path throughput testing (500 errors/sec)

Test Data Management Strategy

  • Fixtures: YAML-based fixtures for middleware configuration and test data sets
  • Factories: Factory pattern for generating realistic middleware pipeline configurations
  • Seeding: Database seed scripts with pre-defined test user and pipeline configurations
  • Isolation: Per-test database transaction rollback for clean state between tests

Test Environment Setup

  • Local Development: Docker Compose stack with test database, Redis cache, and mock services
  • CI/CD Pipeline: Parallel test execution with coverage reporting via JaCoCo and SonarQube
  • Test Data Lifecycle: Automated cleanup of test data after each test run with retention policy for debugging

Test Automation Framework

  • Framework: JUnit 5 + Mockito + Testcontainers + WireMock for comprehensive test automation
  • Test Categories:
    • Fast unit tests (execution time < 10ms)
    • Medium integration tests (execution time < 500ms)
    • Slow end-to-end tests (execution time > 1s, run in CI/CD only)
  • Parallel Execution: Configure test suites to run in parallel with proper resource isolation

Test Coverage Metrics

  • Code Coverage: Target 85% line coverage, 90% branch coverage, with specific focus on middleware logic branches
  • Mutation Testing: Use PIT mutation testing to ensure test quality and identify gaps in test coverage
  • Performance Regression Testing: Automated performance baseline checks to prevent pipeline degradation over time

Test Data Validation

  • Schema Validation: JSON Schema validation for all middleware configuration inputs
  • State Consistency: Verify middleware state transitions and data integrity across pipeline stages
  • Error Recovery Testing: Validate that error handling paths properly restore system state and do not leave partial processing states

Test Reporting and Analysis

  • Test Reports: Generate detailed test reports with failure analysis, performance metrics, and coverage gaps
  • Automated Alerts: Configure Slack alerts for test failures or coverage drops exceeding 5% threshold
  • Test Health Dashboard: Real-time dashboard showing test suite health, coverage trends, and performance metrics

13. Observability Design

Structured Logging Schema

All logs must follow a consistent schema to enable effective querying and analysis. The logging structure includes:

{
  "timestamp": "2023-10-15T14:30:45.123Z",
  "level": "INFO",
  "service": "middleware-pipeline",
  "trace_id": "12345678-1234-1234-1234-123456789012",
  "span_id": "abcdef12-3456-7890-abcd-ef1234567890",
  "user_id": "user_12345",
  "request_id": "req_98765",
  "component": "middleware-pipeline",
  "event_type": "middleware_request_processed",
  "duration_ms": 150,
  "status_code": 200,
  "error_code": null,
  "error_message": null,
  "request_path": "/api/v1/process",
  "request_method": "POST",
  "request_headers": {},
  "response_headers": {},
  "payload_size_bytes": 1024,
  "pipeline_steps": ["auth", "validation", "transformation"],
  "pipeline_status": "success"
}

PII masking rules:

  • User IDs are masked in logs (e.g., user_12345 → user_***)
  • Email addresses are truncated (e.g., john.doe@example.com → j***@example.com)
  • IP addresses are masked (e.g., 192.168.1.100 → 192.168.1.***)

Distributed Tracing Instrumentation

We use OpenTelemetry for distributed tracing with custom spans:

Key Spans:

  1. middleware.pipeline.start - Marks beginning of pipeline processing
  2. middleware.step.execute - Per-step execution with step name as attribute
  3. middleware.pipeline.complete - Marks completion of pipeline

Attributes Captured:

  • pipeline.id: Unique identifier for the pipeline instance
  • step.name: Name of current middleware step (e.g., "auth", "validation")
  • step.duration_ms: Execution time for individual step
  • pipeline.status: success/failure/error
  • error.code: Error code if applicable

Metrics Design

RED Method Metrics (Rate, Errors, Duration):

Metric NameTypeLabelsDescription
middleware_requests_totalCountermethod, path, status_codeTotal number of requests processed
middleware_errors_totalCountermethod, path, error_typeTotal number of errors encountered
middleware_duration_secondsHistogrammethod, path, step_nameRequest processing duration in seconds
middleware_step_errors_totalCounterstep_name, error_typeErrors per middleware step
middleware_pipeline_statusGaugepipeline_id, statusCurrent status of pipeline instance

Additional Metrics:

  • middleware_queue_depth (Gauge): Current queue depth for async processing
  • middleware_concurrent_requests (Gauge): Currently active pipeline executions
  • middleware_step_processing_time_seconds (Histogram): Per-step processing time distribution

Health Check Endpoint Design

GET /health
GET /ready
GET /live
GET /startup

Response Format:

{
  "status": "healthy",
  "timestamp": "2023-10-15T14:30:45.123Z",
  "service": "middleware-pipeline",
  "version": "v1.2.3",
  "checks": {
    "database": {
      "status": "healthy",
      "latency_ms": 5
    },
    "cache": {
      "status": "healthy",
      "latency_ms": 2
    },
    "message_queue": {
      "status": "healthy",
      "latency_ms": 10
    }
  }
}

Health Check Details:

  • /health: Overall service health (database, cache, queue)
  • /ready: Service ready for traffic (all dependencies available)
  • /live: Service process is alive (basic liveness check)
  • /startup: Service startup completion status

Alerting Thresholds

MetricWarning LevelCritical LevelAlerting Path
middleware_errors_total (rate over 5m)> 10 errors/minute> 50 errors/minuteOn-call SRE team
middleware_duration_seconds (p95)> 500ms> 2sOn-call SRE team
middleware_step_errors_total (rate over 5m)> 5 errors/minute> 20 errors/minuteOn-call SRE team
middleware_pipeline_status (gauge)status="error"status="critical"On-call SRE team
middleware_queue_depth> 1000 items> 5000 itemsOn-call SRE team
middleware_concurrent_requests> 1000 concurrent> 5000 concurrentOn-call SRE team

Escalation Path:

  1. First alert → Slack notification to dev team
  2. Second alert (within 5 minutes) → On-call SRE escalation
  3. Third alert (within 10 minutes) → PagerDuty alert to SRE team

Alerting Conditions:

  • Error rate thresholds for both overall and per-step errors
  • Latency thresholds with p95/p99 percentiles for request processing
  • Queue depth warnings to prevent system overload
  • Concurrent request thresholds to detect potential DoS or performance degradation

14. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering alignment

  • Middleware ordering and composition strategy: Should middleware be configured per route or globally? How do we handle middleware that needs to run before/after specific routes vs. all routes?
  • Error handling in middleware: Should middleware be allowed to handle errors or should they only transform requests/responses? This affects the error propagation model.
  • Middleware lifecycle management: How do we handle middleware initialization, shutdown, and configuration updates without downtime?
  • Performance impact of middleware: Should we allow middleware to be disabled per route or require all middleware to run for every request?

Known trade-offs and justifications

  • Flexibility vs. Performance: The middleware pipeline design prioritizes flexibility (supporting various types of middleware with custom logic) over raw performance. Each middleware function adds a layer of indirection and potential overhead.
  • Simplicity vs. Extensibility: Using a simple function composition approach (e.g., next(middleware(request))) makes the design easy to understand but limits advanced features like conditional middleware execution or complex routing logic.
  • Type safety vs. generic handling: The design uses generic types (T) to support different request/response types, but this reduces compile-time type safety compared to strongly-typed middleware functions.
  • Synchronous vs. asynchronous execution: Middleware is currently designed for synchronous execution, but async middleware support would require more complex error handling and threading considerations.

Risks and mitigation strategies

  • Performance degradation: Middleware execution adds overhead to every request. Risk mitigation includes:

    • Performance monitoring and alerting for middleware execution times
    • Middleware prioritization (critical vs. non-critical)
    • Caching expensive middleware computations where possible
    • Profiling and benchmarking during development
  • Error propagation complexity: Middleware errors can be hard to trace and debug. Risk mitigation includes:

    • Comprehensive error logging with stack traces
    • Distributed tracing integration to track middleware execution flow
    • Middleware-specific error handling and fallback strategies
  • Security vulnerabilities: Middleware that modifies requests/responses can introduce security issues. Risk mitigation includes:

    • Input validation and sanitization in middleware
    • Security-focused middleware (rate limiting, authentication) placed early in pipeline
    • Regular security audits and penetration testing of middleware components
  • Configuration drift: Middleware configuration can become inconsistent across environments. Risk mitigation includes:

    • Configuration management with version control
    • Automated testing of middleware configurations
    • CI/CD pipeline validation for middleware changes

Technical debt acknowledged

  • Limited middleware introspection: The current design doesn't provide easy ways to inspect or debug middleware execution flow, which could make troubleshooting difficult in production.
  • No built-in middleware metrics: There's no automatic instrumentation of middleware execution times or success/failure rates, requiring manual implementation for observability.
  • Hardcoded error handling: Error handling in middleware is currently not standardized, leading to inconsistent error responses across different middleware components.
  • No support for conditional middleware: The pipeline doesn't support conditional middleware execution based on request properties, which limits flexibility in complex routing scenarios.
  • Limited testing patterns: The design doesn't enforce specific testing patterns for middleware, which could lead to inconsistent test coverage and maintainability issues.

Was this article helpful?

Topics in this article

More from Backend LLD

  • Backend LLD

    Request/Response Transformation Pipeline Design

    Comprehensive low-level design for building scalable request/response transformation pipelines with support for multiple formats, validation, and error handling.

  • Backend LLD

    Template Method Pattern Framework Design

    Comprehensive low-level design for a flexible template method pattern implementation with extensibility and performance considerations

  • 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.

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.