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
- What specific middleware functionality are we designing for? (e.g., authentication, logging, rate limiting, request/response transformation)
- What is the expected scale of concurrent requests and throughput requirements?
- Are there specific protocols or formats (HTTP, gRPC, WebSockets) the middleware pipeline must support?
- What are the key entities and their relationships in the system that will be processed by this middleware?
- What is the expected latency sensitivity for different types of middleware operations (e.g., auth checks vs. logging)?
- Do we need to support configuration-driven middleware chains or hardcoded pipeline definitions?
- 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:
- Strong typing: gRPC's Protocol Buffers provide clear, versioned contracts that are essential for middleware systems where components must interoperate reliably
- Performance: Binary serialization is more efficient than JSON for middleware traffic, especially in high-throughput scenarios
- Bidirectional streaming: Middleware often requires real-time communication between components
- Service contracts: gRPC services provide a clear interface contract that can be versioned and evolved systematically
- 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
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| CreatePipeline | /v1/pipelines | Create a new middleware pipeline configuration | JWT | 100 req/min |
| GetPipeline | /v1/pipelines/{id} | Retrieve pipeline configuration by ID | JWT | 500 req/min |
| UpdatePipeline | /v1/pipelines/{id} | Update existing pipeline configuration | JWT | 50 req/min |
| DeletePipeline | /v1/pipelines/{id} | Soft delete pipeline configuration | JWT | 25 req/min |
| ListPipelines | /v1/pipelines | List all pipelines with filtering and pagination | JWT | 200 req/min |
| StartPipeline | /v1/pipelines/{id}/start | Activate a pipeline for processing | JWT | 50 req/min |
| StopPipeline | /v1/pipelines/{id}/stop | Deactivate a pipeline from processing | JWT | 50 req/min |
| GetPipelineStatus | /v1/pipelines/{id}/status | Get current processing status of pipeline | JWT | 1000 req/min |
| GetPipelineMetrics | /v1/pipelines/{id}/metrics | Retrieve performance metrics for pipeline | JWT | 200 req/min |
| ValidatePipeline | /v1/pipelines/validate | Validate pipeline configuration before creation | JWT | 100 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 Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request - Invalid input |
| 401 | Unauthorized - Missing or invalid auth |
| 403 | Forbidden - Insufficient permissions |
| 404 | Not Found - Resource doesn't exist |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal 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 pagelimit: Number of items per page (default: 20, max: 100)sort: Sort field (created_at,name)order: Sort order (ASCorDESC)
Response Format:
{
"items": [ /* pipeline objects */ ],
"nextCursor": "base64encodedcursor",
"hasMore": true,
"totalCount": 1000
}
Justification: Cursor-based pagination is preferred over offset-based because:
- It's more efficient for large datasets
- It handles data changes gracefully (inserts/deletes don't shift page boundaries)
- 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 pipelinesPIPELINE_EDITOR: Read/write access to owned pipelinesPIPELINE_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:
- Store idempotency keys with TTL (1 hour)
- For duplicate requests, return the same response as the first request
- 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
-
Composite Indexes:
pipeline_id, stage_orderonmiddleware_pipeline_stagefor efficient pipeline traversalpipeline_id, config_keyonmiddleware_pipeline_configfor fast configuration lookups
-
Partial Indexes:
WHERE is_active = TRUEonmiddleware_pipelineto optimize active pipeline queriesWHERE log_level IN ('ERROR', 'FATAL')onmiddleware_pipeline_logfor error monitoring
-
Covering Indexes:
pipeline_id, status, is_activeonmiddleware_pipelineto 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_attimestampsowner_idfor ownership trackingstatusfield 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_logtable 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:
- MiddlewarePipeline: The core orchestrator that manages middleware execution order and context propagation
- Middleware: Interface defining the contract for middleware components to implement
- Context: Container for request/response data and metadata that flows through the pipeline
- Next: Callback interface to continue pipeline execution to the next middleware
- 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:
- Immutable state objects - All middleware configurations and request context objects will be immutable to prevent concurrent modification
- Thread-local storage - Per-request state will be stored in thread-local variables to avoid cross-contamination
- Synchronized access - Critical shared resources (e.g., global configuration, metrics counters) will use synchronized blocks or atomic references
- Lock-free data structures - For high-performance scenarios, concurrent collections like
ConcurrentHashMapandAtomicReferencewill 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:
- Fixed ordering of resource acquisition - All middleware components will acquire locks in a predefined order (e.g., by component name or priority)
- Timeout-based lock acquisition - All lock acquisitions will have a timeout (500ms) to prevent indefinite blocking
- Avoid nested locks - Middleware components will not hold multiple locks simultaneously
- 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
- Immutable Configuration Objects - All middleware configurations will be immutable to ensure thread safety
- Thread-Safe Component Registration - Middleware component registration will use concurrent collections and synchronized initialization
- Atomic State Transitions - Pipeline state changes will be atomic using
AtomicReferenceor similar constructs - 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 Type | TTL | Reason |
|---|---|---|
| Pipeline Configurations | 5 minutes | Frequent updates during deployment |
| Middleware Rules | 1 hour | Stable once deployed |
| Auth Tokens | 30 minutes | Session timeout requirements |
| Rate Limiting Buckets | 1 minute | Per-second granularity needed |
| User Context | 15 minutes | Recent 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
- Warm-up strategy: Preload critical pipeline configurations during service startup
- Cache hit ratio monitoring: Track and alert on sub-50% hit ratios
- Memory usage tracking: Monitor L1 and Redis memory consumption
- 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:
- Performance: Raw queries eliminate ORM overhead and allow for fine-tuned optimization
- Control: Direct query control is needed for complex middleware logic and filtering
- 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
- Single Responsibility: Each repository handles one domain entity type
- Consistent API: All repositories implement the same interface contract
- Performance Focus: Raw SQL queries with proper indexing and batching
- Transaction Safety: Clear transaction boundaries for data consistency
- 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:
- 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.
- Metrics and telemetry - Publishing metrics to monitoring systems (e.g., Prometheus, Datadog) should be async to avoid impacting request latency.
- External service notifications - Sending events to external systems (e.g., payment gateway, notification service) should be async to prevent request timeouts.
- 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:
- Messages that fail more than 3 times will be moved to DLQ.
- DLQ messages will be stored in a dedicated topic with a retention policy of 7 days.
- DLQ messages will be tagged with metadata indicating the original topic, error type, and failure count.
Poison Message Handling:
- DLQ messages will be flagged for manual inspection by operations team.
- A separate monitoring dashboard will surface DLQ messages with high failure rates.
- 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:
- Simplicity: The middleware pipeline is designed to be lightweight and focused on request processing.
- Eventual consistency: The asynchronous events (logging, metrics) are acceptable to lose in edge cases.
- Performance: The outbox pattern adds an extra database write and read, which would impact performance for a middleware system.
- 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:
- Local handling: Specific error types are caught and transformed at service boundaries
- Aggregated errors: Multiple errors from pipeline stages are collected and wrapped in a composite error
- 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:
middleware.pipeline.start- Marks beginning of pipeline processingmiddleware.step.execute- Per-step execution with step name as attributemiddleware.pipeline.complete- Marks completion of pipeline
Attributes Captured:
pipeline.id: Unique identifier for the pipeline instancestep.name: Name of current middleware step (e.g., "auth", "validation")step.duration_ms: Execution time for individual steppipeline.status: success/failure/errorerror.code: Error code if applicable
Metrics Design
RED Method Metrics (Rate, Errors, Duration):
| Metric Name | Type | Labels | Description |
|---|---|---|---|
middleware_requests_total | Counter | method, path, status_code | Total number of requests processed |
middleware_errors_total | Counter | method, path, error_type | Total number of errors encountered |
middleware_duration_seconds | Histogram | method, path, step_name | Request processing duration in seconds |
middleware_step_errors_total | Counter | step_name, error_type | Errors per middleware step |
middleware_pipeline_status | Gauge | pipeline_id, status | Current status of pipeline instance |
Additional Metrics:
middleware_queue_depth(Gauge): Current queue depth for async processingmiddleware_concurrent_requests(Gauge): Currently active pipeline executionsmiddleware_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
| Metric | Warning Level | Critical Level | Alerting Path |
|---|---|---|---|
middleware_errors_total (rate over 5m) | > 10 errors/minute | > 50 errors/minute | On-call SRE team |
middleware_duration_seconds (p95) | > 500ms | > 2s | On-call SRE team |
middleware_step_errors_total (rate over 5m) | > 5 errors/minute | > 20 errors/minute | On-call SRE team |
middleware_pipeline_status (gauge) | status="error" | status="critical" | On-call SRE team |
middleware_queue_depth | > 1000 items | > 5000 items | On-call SRE team |
middleware_concurrent_requests | > 1000 concurrent | > 5000 concurrent | On-call SRE team |
Escalation Path:
- First alert → Slack notification to dev team
- Second alert (within 5 minutes) → On-call SRE escalation
- 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.