Skip to main content

Command Pattern Framework Design and Implementation

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

The command pattern framework provides a robust mechanism for encapsulating all information about an operation, enabling features like undo/redo functionality, logging, and extensible command execution in enterprise backend systems.

1. Problem Statement & Requirements

Clarifying questions

  1. What is the expected scale of command execution? Are we designing for thousands of commands per second or more?
  2. What consistency requirements are there? Should all command executions be strongly consistent, or can eventual consistency be acceptable?
  3. What are the key entities in the command execution lifecycle? Is there a need to track command state, status, or audit logs?
  4. What is the read/write ratio for command metadata? Will there be frequent reads of command history or status?
  5. How sensitive is the system to latency? Are there time-critical commands that must complete within specific SLAs?
  6. What authentication and authorization model is expected? Should commands be restricted to specific roles or users?
  7. Are there external integrations or dependencies that commands might trigger? Should the framework support async command execution with retries?

Functional requirements

  • Command Registration: The system must allow registration of commands with associated handlers.
  • Command Execution: Commands must be executable by their registered handler.
  • Command Metadata Tracking: System must track command execution status, timestamps, and result details.
  • Command Retry Mechanism: Commands that fail should support retry logic with configurable backoff strategies.
  • Asynchronous Command Execution: Support for async command execution to prevent blocking the main thread.
  • Command State Management: Commands must have a lifecycle (pending, executing, completed, failed).
  • Idempotency Support: Commands must be designed to support idempotent execution where appropriate.

Non-functional requirements

  • Latency Targets:
    • p50: < 50ms for command execution
    • p95: < 200ms for command execution
    • p99: < 500ms for command execution
  • Throughput: Support at least 10,000 commands per second.
  • Availability: 99.9% uptime with graceful degradation for partial failures.
  • Scalability: Support horizontal scaling of command execution across multiple nodes.

Constraints and assumptions

  • Assumption: Commands are primarily used for business logic operations like order placement, user creation, or payment processing.
  • Assumption: Commands will be executed in a single-threaded context for simplicity, but can be extended to support async execution.
  • Constraint: The system assumes commands are not long-running or blocking operations.
  • Assumption: Command execution is bounded by a timeout to prevent indefinite hanging.

Out of scope

  • Command Scheduling: Scheduling commands for future execution is out of scope.
  • Cross-service Command Orchestration: Distributed command coordination or transactional consistency across services is out of scope.
  • UI/Console Management: Admin UI or console for managing commands is out of scope.
  • Command Versioning: Supporting multiple versions of a command is out of scope.
  • Audit Trail for Command Metadata: Detailed audit trail of all command metadata changes is out of scope.

2. API Design

API Style Selection

For a Command pattern framework, I'll select gRPC as the primary API style. This choice is driven by:

  1. Strong typing: gRPC's protobuf definitions provide compile-time type safety, which is crucial for a framework that needs to handle diverse command types
  2. Performance: Binary protocol with efficient serialization is ideal for command execution workloads
  3. Built-in support for streaming: Enables real-time feedback and progress tracking for long-running commands
  4. Language agnostic: Supports multiple client languages (Java, Go, Python, etc.) for framework extensibility
  5. Service contracts: Clear separation between command interface and implementation

Endpoint Definitions

MethodPathDescriptionAuth
ExecuteCommand/command.v1.CommandService/ExecuteCommandExecutes a single command with payload and metadataJWT/OAuth2
BatchExecuteCommands/command.v1.CommandService/BatchExecuteCommandsExecutes multiple commands in batch modeJWT/OAuth2
GetCommandStatus/command.v1.CommandService/GetCommandStatusRetrieves status and result of a previously executed commandJWT/OAuth2
CancelCommand/command.v1.CommandService/CancelCommandRequests cancellation of a running commandJWT/OAuth2
ListCommands/command.v1.CommandService/ListCommandsLists commands with filtering and paginationJWT/OAuth2

Request/Response Schemas

ExecuteCommandRequest

message ExecuteCommandRequest {
  string command_id = 1;
  string command_type = 2;
  bytes payload = 3;
  map<string, string> metadata = 4;
  int64 timeout_ms = 5;
  bool async = 6;
}

ExecuteCommandResponse

message ExecuteCommandResponse {
  string command_id = 1;
  CommandStatus status = 2;
  bytes result = 3;
  string error_message = 4;
  int64 execution_time_ms = 5;
}

CommandStatus

enum CommandStatus {
  PENDING = 0;
  RUNNING = 1;
  SUCCESS = 2;
  FAILED = 3;
  CANCELLED = 4;
}

CommandListRequest

message CommandListRequest {
  string user_id = 1;
  string command_type = 2;
  CommandStatus status = 3;
  int32 page_size = 4;
  string page_token = 5;
}

CommandListResponse

message CommandListResponse {
  repeated CommandInfo commands = 1;
  string next_page_token = 2;
}

HTTP Status Codes and Error Format

Standard HTTP Status Codes:

  • 200 OK - Successful command execution
  • 202 Accepted - Async command accepted for processing
  • 400 Bad Request - Invalid command parameters
  • 401 Unauthorized - Missing or invalid authentication
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Command not found
  • 429 Too Many Requests - Rate limiting exceeded
  • 500 Internal Server Error - System error

Standardized Error Response Format:

{
  "error": {
    "code": "COMMAND_EXECUTION_FAILED",
    "message": "Command failed due to validation error",
    "details": {
      "command_id": "cmd-12345",
      "timestamp": "2023-12-01T10:30:00Z",
      "retry_count": 2
    },
    "http_status": 500
  }
}

API Versioning Strategy

Versioning will follow semantic versioning with path-based versioning:

  • /command.v1.CommandService/ExecuteCommand - v1
  • /command.v2.CommandService/ExecuteCommand - v2 (for breaking changes)

Versioning will be handled at the service level with backward compatibility maintained through:

  1. Default parameter values for new optional fields
  2. Migration paths for breaking changes
  3. Deprecation warnings for older versions

Pagination Design

Cursor-based pagination will be used for the ListCommands endpoint because:

  1. Stability: Cursor-based pagination is not affected by data changes during pagination
  2. Consistency: Provides consistent results even when items are inserted/deleted between requests
  3. Performance: More efficient than offset-based pagination for large datasets

Authentication and Authorization

Authentication:

  • JWT tokens with OAuth2 integration for service-to-service authentication
  • API key support for client applications (with proper rate limiting)
  • Session-based authentication for web UI clients

Authorization:

  • RBAC (Role-Based Access Control) model with roles:
    • command_executor - Can execute commands
    • command_admin - Can cancel, view all commands
    • command_reader - Can only read command status

Rate Limiting Strategy

Rate limiting will be implemented per endpoint:

EndpointLimitWindowEnforcement
ExecuteCommand1000 req/min1 minuteIn-memory + Redis
BatchExecuteCommands500 req/min1 minuteIn-memory + Redis
GetCommandStatus5000 req/min1 minuteIn-memory + Redis
CancelCommand200 req/min1 minuteIn-memory + Redis

Enforcement Strategy:

  • Distributed token bucket algorithm using Redis
  • Per-user and per-service rate limits
  • Burst handling with configurable burst size

Idempotency Design

Idempotency Key Strategy:

  1. Clients provide optional idempotency_key in ExecuteCommandRequest
  2. Server uses this key to detect duplicate requests within 5 minutes
  3. If duplicate detected, return cached result with updated timestamp
// Idempotency key validation logic
public ExecuteCommandResponse executeWithIdempotency(ExecuteCommandRequest request) {
    String idempotencyKey = request.getIdempotencyKey();
    if (idempotencyKey != null) {
        CommandResult cached = commandCache.get(idempotencyKey);
        if (cached != null) {
            return buildResponseFromCache(cached, request.getCommandId());
        }
    }
    
    // Process command normally
    ExecuteCommandResponse response = executeCommand(request);
    
    if (idempotencyKey != null) {
        commandCache.put(idempotencyKey, response);
    }
    
    return response;
}

3. Data Model Design

Entity-Relationship diagram (Mermaid)

erDiagram
    COMMAND {
        string id PK
        string name
        string description
        string command_class
        string status
        string created_by
        timestamp created_at
        timestamp updated_at
        jsonb parameters
        jsonb metadata
    }
    
    COMMAND_EXECUTION {
        string id PK
        string command_id FK
        string status
        timestamp started_at
        timestamp completed_at
        string result
        string error_message
        jsonb execution_context
        timestamp created_at
    }
    
    COMMAND_HISTORY {
        string id PK
        string command_id FK
        string status
        timestamp executed_at
        string result
        string error_message
        jsonb execution_context
    }
    
    COMMAND_DEPENDENCY {
        string command_id FK
        string depends_on_command_id FK
        string dependency_type
    }

    COMMAND ||--o{ COMMAND_EXECUTION : "has"
    COMMAND ||--o{ COMMAND_HISTORY : "has"
    COMMAND ||--o{ COMMAND_DEPENDENCY : "depends_on"
    COMMAND_DEPENDENCY ||--o{ COMMAND : "is_dependent"

Table / Collection schema definitions with field types, constraints, indices

commands table

CREATE TABLE commands (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    command_class VARCHAR(255) NOT NULL,
    status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
    created_by VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    parameters JSONB,
    metadata JSONB,
    
    CONSTRAINT chk_command_status CHECK (status IN ('PENDING', 'PROCESSING', 'SUCCESS', 'FAILED', 'CANCELLED')),
    CONSTRAINT chk_command_class_not_empty CHECK (command_class != '')
);

CREATE INDEX idx_commands_name ON commands(name);
CREATE INDEX idx_commands_status ON commands(status);
CREATE INDEX idx_commands_created_by ON commands(created_by);
CREATE INDEX idx_commands_created_at ON commands(created_at);
CREATE INDEX idx_commands_command_class ON commands(command_class);

command_executions table

CREATE TABLE command_executions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    command_id UUID NOT NULL REFERENCES commands(id) ON DELETE CASCADE,
    status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
    started_at TIMESTAMP WITH TIME ZONE,
    completed_at TIMESTAMP WITH TIME ZONE,
    result TEXT,
    error_message TEXT,
    execution_context JSONB,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT chk_execution_status CHECK (status IN ('PENDING', 'PROCESSING', 'SUCCESS', 'FAILED')),
    CONSTRAINT chk_started_before_completed CHECK (
        (started_at IS NULL AND completed_at IS NULL) OR 
        (started_at IS NOT NULL AND completed_at IS NOT NULL)
    )
);

CREATE INDEX idx_command_executions_command_id ON command_executions(command_id);
CREATE INDEX idx_command_executions_status ON command_executions(status);
CREATE INDEX idx_command_executions_started_at ON command_executions(started_at);

command_history table

CREATE TABLE command_history (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    command_id UUID NOT NULL REFERENCES commands(id) ON DELETE CASCADE,
    status VARCHAR(50) NOT NULL,
    executed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    result TEXT,
    error_message TEXT,
    execution_context JSONB,
    
    CONSTRAINT chk_history_status CHECK (status IN ('SUCCESS', 'FAILED'))
);

CREATE INDEX idx_command_history_command_id ON command_history(command_id);
CREATE INDEX idx_command_history_executed_at ON command_history(executed_at);

command_dependencies table

CREATE TABLE command_dependencies (
    command_id UUID NOT NULL REFERENCES commands(id) ON DELETE CASCADE,
    depends_on_command_id UUID NOT NULL REFERENCES commands(id) ON DELETE CASCADE,
    dependency_type VARCHAR(50) NOT NULL DEFAULT 'SEQUENTIAL',
    
    PRIMARY KEY (command_id, depends_on_command_id),
    CONSTRAINT chk_dependency_type CHECK (dependency_type IN ('SEQUENTIAL', 'PARALLEL'))
);

CREATE INDEX idx_command_dependencies_depends_on ON command_dependencies(depends_on_command_id);

Primary key strategy (UUID vs. auto-increment vs. ULID — with justification)

Using UUID as primary key for all tables:

  • UUIDs provide globally unique identifiers across distributed systems
  • Eliminates potential conflicts in multi-region deployments
  • Supports horizontal sharding with predictable distribution
  • Enables better audit trails and referential integrity in distributed environments
  • No need to coordinate sequence generation across services

Index design (composite, partial, covering indices — with justification per index)

Composite indexes:

  • idx_commands_name - Composite on name for fast lookups by command name
  • idx_command_executions_command_id - Composite on command_id for fast execution history lookups
  • idx_command_history_command_id - Composite on command_id for historical execution tracking

Partial indexes:

  • idx_commands_status - Partial index on status to optimize queries filtering by status
  • idx_command_executions_started_at - Partial index on started_at to optimize active execution queries

Covering indexes:

  • idx_commands_created_by - Covers queries that filter by creator and return basic command info
  • idx_command_history_executed_at - Covers historical execution queries with minimal table access

Data partitioning / sharding strategy

Partition by created_at date range:

  • Daily partitions for recent commands (last 30 days)
  • Monthly partitions for older commands (30-365 days)
  • Quarterly partitions for very old commands (> 365 days)

Partition key: created_at::date

  • Enables time-based query optimization
  • Supports retention policies and archiving
  • Allows for efficient historical analysis

Soft delete vs. hard delete decision with justification

Soft delete using status field:

  • Commands are marked as CANCELLED rather than deleted
  • Maintains audit trail and historical context
  • Enables recovery of accidentally cancelled commands
  • Supports compliance requirements for data retention
  • Allows for status-based filtering and reporting

Audit trail design (who, what, when)

All command operations are audited:

  • created_by field captures the operator identity
  • created_at and updated_at timestamps capture timing
  • status field tracks state transitions
  • metadata field can store additional audit context

Data archival strategy and retention policy

Archival strategy:

  • Commands older than 1 year are archived to cold storage
  • Historical execution data is retained for 2 years
  • Successful command executions are kept for 30 days in active storage

Retention policy:

-- Archive procedure example (simplified)
CREATE OR REPLACE PROCEDURE archive_old_commands()
LANGUAGE plpgsql
AS $$
BEGIN
    -- Move commands older than 1 year to archive tables
    INSERT INTO commands_archive SELECT * FROM commands WHERE created_at < NOW() - INTERVAL '1 year';
    DELETE FROM commands WHERE created_at < NOW() - INTERVAL '1 year';
END;
$$;

4. Class Design & Service Layer

UML Class Diagram

classDiagram
    class Command {
        <<interface>>
        +execute()
        +rollback()
    }
    
    class CommandContext {
        -commandId: String
        -userId: String
        -timestamp: Instant
        -metadata: Map~String, Object~
        +getCommandId()
        +getUserId()
        +getTimestamp()
        +getMetadata()
    }
    
    class CommandHandler {
        <<interface>>
        +handle(command: Command, context: CommandContext)
        +validate(command: Command)
    }
    
    class CommandProcessor {
        -commandHandlers: Map~Class~CommandHandler~
        -executorService: ExecutorService
        -eventPublisher: EventPublisher
        -logger: Logger
        +process(command: Command, context: CommandContext)
        +registerHandler(commandType: Class~Command~, handler: CommandHandler)
    }
    
    class CommandResult {
        -success: boolean
        -resultData: Object
        -error: CommandError
        -executionTime: Duration
    }
    
    class CommandError {
        -errorCode: String
        -message: String
        -details: Map~String, Object~
    }
    
    class CommandExecutionException {
        -commandId: String
        -originalError: Throwable
    }
    
    class CommandFactory {
        +createCommand(commandType: String, payload: Map~String, Object~)
    }
    
    class RetryableCommand {
        -maxRetries: int
        -retryDelay: Duration
        +executeWithRetry()
    }
    
    class CommandAuditTrail {
        -commandId: String
        -userId: String
        -commandType: String
        -status: CommandStatus
        -timestamp: Instant
        -executionTime: Duration
    }
    
    class CommandStatus {
        PENDING
        EXECUTING
        SUCCESS
        FAILED
        ROLLED_BACK
    }
    
    Command <|-- RetryableCommand
    CommandHandler <|-- CommandProcessor
    CommandProcessor --> CommandContext : uses
    CommandProcessor --> CommandResult : returns
    CommandProcessor --> EventPublisher : publishes events
    CommandProcessor --> CommandAuditTrail : logs audit
    CommandHandler --> Command : handles
    CommandFactory --> Command : creates
    
    class Command {
        +execute()
        +rollback()
    }
    
    class CommandHandler {
        +handle(command: Command, context: CommandContext)
        +validate(command: Command)
    }
    
    class CommandProcessor {
        -commandHandlers: Map~Class, CommandHandler~
        +process(command: Command, context: CommandContext)
        +registerHandler(commandType: Class, handler: CommandHandler)
    }

State Machine Diagram for Command

stateDiagram-v2
    [*] --> PENDING
    PENDING --> EXECUTING : process()
    EXECUTING --> SUCCESS : execute() succeeds
    EXECUTING --> FAILED : execute() fails
    FAILED --> ROLLED_BACK : rollback() succeeds
    FAILED --> FAILED : rollback() fails
    SUCCESS --> [*]
    ROLLED_BACK --> [*]

Service Class Structure and Responsibilities

The Command pattern framework consists of several key service classes:

  1. CommandProcessor: Main orchestrator that handles command execution lifecycle, including validation, execution, rollback, and audit logging.
  2. CommandHandler: Interface for implementing command-specific business logic.
  3. CommandContext: Immutable context container for command execution metadata.
  4. CommandFactory: Factory for creating command instances from serialized payloads.
  5. CommandResult: Encapsulates the result of command execution including success/failure status and any error details.

Interface / Abstract Class Design

public interface Command {
    String getCommandId();
    String getCommandType();
    Map<String, Object> getPayload();
    default void validate() {
        // Default no-op validation
    }
    
    Object execute(CommandContext context) throws CommandExecutionException;
    void rollback(CommandContext context) throws CommandExecutionException;
}

public interface CommandHandler {
    Object handle(Command command, CommandContext context) throws CommandExecutionException;
    default void validate(Command command) throws CommandValidationException {
        // Default no-op validation
    }
}

public abstract class AbstractCommand implements Command {
    protected final String commandId;
    protected final String commandType;
    protected final Map<String, Object> payload;
    
    public AbstractCommand(String commandId, String commandType, Map<String, Object> payload) {
        this.commandId = commandId;
        this.commandType = commandType;
        this.payload = payload != null ? new HashMap<>(payload) : new HashMap<>();
    }
    
    @Override
    public String getCommandId() {
        return commandId;
    }
    
    @Override
    public String getCommandType() {
        return commandType;
    }
    
    @Override
    public Map<String, Object> getPayload() {
        return new HashMap<>(payload);
    }
    
    @Override
    public void validate() {
        // Default validation logic can be overridden by subclasses
    }
}

Business Logic Encapsulation Patterns

The framework implements the Strategy pattern for command handling, allowing different strategies to be applied based on command type. The CommandProcessor uses a registry pattern to dynamically dispatch commands to their appropriate handlers.

Critical-Path Method Implementation

public CommandResult process(Command command, CommandContext context) {
    String commandId = command.getCommandId();
    Instant startTime = Instant.now();
    
    try {
        // 1. Validate command
        validateCommand(command, context);
        
        // 2. Log audit trail - START
        CommandAuditTrail audit = new CommandAuditTrail(
            commandId, 
            context.getUserId(), 
            command.getCommandType(), 
            CommandStatus.EXECUTING, 
            startTime,
            null
        );
        auditTrailService.logAudit(audit);
        
        // 3. Execute command with retry logic
        Object result = executeWithRetry(command, context);
        
        // 4. Log success audit trail
        Duration executionTime = Duration.between(startTime, Instant.now());
        auditTrailService.logAudit(new CommandAuditTrail(
            commandId,
            context.getUserId(),
            command.getCommandType(),
            CommandStatus.SUCCESS,
            startTime,
            executionTime
        ));
        
        // 5. Publish success event
        eventPublisher.publish(new CommandExecutedEvent(commandId, result));
        
        return new CommandResult(true, result, null, executionTime);
        
    } catch (CommandExecutionException e) {
        // 6. Handle failure - attempt rollback if needed
        try {
            command.rollback(context);
            
            // 7. Log audit trail - FAILED and ROLLED_BACK
            Duration executionTime = Duration.between(startTime, Instant.now());
            auditTrailService.logAudit(new CommandAuditTrail(
                commandId,
                context.getUserId(),
                command.getCommandType(),
                CommandStatus.ROLLED_BACK,
                startTime,
                executionTime
            ));
            
            // 8. Publish failure event
            eventPublisher.publish(new CommandFailedEvent(commandId, e));
            
            return new CommandResult(false, null, 
                new CommandError("COMMAND_EXECUTION_FAILED", e.getMessage(), null), 
                executionTime);
                
        } catch (CommandExecutionException rollbackEx) {
            // 9. Log final failure state
            Duration executionTime = Duration.between(startTime, Instant.now());
            auditTrailService.logAudit(new CommandAuditTrail(
                commandId,
                context.getUserId(),
                command.getCommandType(),
                CommandStatus.FAILED,
                startTime,
                executionTime
            ));
            
            // 10. Publish failure event with rollback error
            eventPublisher.publish(new CommandFailedEvent(commandId, e));
            
            return new CommandResult(false, null, 
                new CommandError("COMMAND_ROLLBACK_FAILED", 
                    "Command failed and rollback also failed: " + rollbackEx.getMessage(), null), 
                executionTime);
        }
    }
}

private Object executeWithRetry(Command command, CommandContext context) 
    throws CommandExecutionException {
    
    int maxRetries = 3;
    Duration retryDelay = Duration.ofSeconds(1);
    
    for (int attempt = 0; attempt <= maxRetries; attempt++) {
        try {
            return command.execute(context);
        } catch (CommandExecutionException e) {
            if (attempt == maxRetries) {
                throw e;
            }
            
            // Exponential backoff with jitter
            long delayMs = (long) (retryDelay.toMillis() * Math.pow(2, attempt));
            long jitter = ThreadLocalRandom.current().nextLong(0, delayMs / 2);
            try {
                Thread.sleep(delayMs + jitter);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                throw new CommandExecutionException("Retry interrupted", ie);
            }
        }
    }
    
    throw new CommandExecutionException("Max retries exceeded");
}

Transaction Boundary Design

Transaction boundaries in the command framework are defined by:

  1. Single command execution - Each command operation is atomic within a single transaction scope.
  2. Command handler isolation - Each command handler operates in its own transaction context.
  3. Distributed transactions - For cross-service commands, the framework supports Saga orchestration patterns.

Service Interface Contracts

public interface CommandProcessor {
    CommandResult process(Command command, CommandContext context);
    void registerHandler(Class<? extends Command> commandType, CommandHandler handler);
    CommandResult processAsync(Command command, CommandContext context);
}

public interface CommandFactory {
    Command createCommand(String commandType, Map<String, Object> payload);
}

Saga / Orchestration Pattern

For distributed commands that span multiple services, the framework supports a Saga pattern:

public class DistributedCommandSaga {
    private final List<CommandStep> steps;
    private final String sagaId;
    
    public void execute() {
        for (int i = 0; i < steps.size(); i++) {
            try {
                CommandStep step = steps.get(i);
                step.execute();
                
                // If this is the last step, commit
                if (i == steps.size() - 1) {
                    commit();
                }
            } catch (Exception e) {
                // Rollback all previous steps
                rollback(i);
                throw new SagaExecutionException("Saga failed at step " + i, e);
            }
        }
    }
    
    private void rollback(int fromIndex) {
        for (int i = fromIndex; i >= 0; i--) {
            steps.get(i).rollback();
        }
    }
}

Domain Object / Value Object Design

public class CommandContext {
    private final String commandId;
    private final String userId;
    private final Instant timestamp;
    private final Map<String, Object> metadata;
    
    public CommandContext(String commandId, String userId, Instant timestamp, 
                         Map<String, Object> metadata) {
        this.commandId = commandId;
        this.userId = userId;
        this.timestamp = timestamp;
        this.metadata = metadata != null ? new HashMap<>(metadata) : new HashMap<>();
    }
    
    // Getters with defensive copying
    public String getCommandId() { return commandId; }
    public String getUserId() { return userId; }
    public Instant getTimestamp() { return timestamp; }
    public Map<String, Object> getMetadata() { 
        return new HashMap<>(metadata); 
    }
}

public class CommandResult {
    private final boolean success;
    private final Object resultData;
    private final CommandError error;
    private final Duration executionTime;
    
    public CommandResult(boolean success, Object resultData, 
                        CommandError error, Duration executionTime) {
        this.success = success;
        this.resultData = resultData;
        this.error = error;
        this.executionTime = executionTime;
    }
    
    // Getters...
}

5. Concurrency & Thread Safety

Concurrency Model

The Command pattern framework will operate in a multi-threaded environment where multiple clients may submit commands concurrently. The framework uses a thread pool executor for command execution, allowing for concurrent processing of independent commands while maintaining control over resource usage. Each command execution runs in a separate thread from the pool, with proper synchronization mechanisms to prevent race conditions during state updates.

Race Condition Prevention Strategy

Race conditions are mitigated through:

  1. Immutable command objects - Command instances are read-only once created, preventing concurrent modification
  2. Synchronized execution context - Command execution is wrapped in synchronized blocks where state changes occur
  3. Atomic operations for shared mutable state using java.util.concurrent.atomic packages
  4. Thread-local storage for per-request context that doesn't need synchronization

Optimistic vs. Pessimistic Locking Selection

For command execution, optimistic locking is preferred over pessimistic locking for the following reasons:

  • Commands are typically idempotent and retryable
  • Reduces lock contention in high-concurrency scenarios
  • Avoids blocking threads during command execution
  • Better performance for read-heavy workloads where writes are infrequent

The framework implements optimistic locking through version fields in entities, with conflict detection during command execution.

Deadlock Prevention Strategy

Deadlock prevention is achieved through:

  1. Fixed ordering of resource acquisition - Commands always acquire locks in a consistent global order
  2. Timeout-based lock acquisition - Locks are acquired with timeout to prevent indefinite blocking
  3. Command dependency graph analysis - Framework prevents circular dependencies in command execution order
  4. Thread interruption handling - Threads are interrupted if they exceed their allocated time for command execution

Idempotency Implementation for Concurrent Duplicate Requests

The framework implements idempotency through:

  1. Idempotency keys - Each command can include an optional idempotency key that uniquely identifies the request
  2. Command result caching - Results of executed commands are cached with idempotency key as the cache key
  3. Duplicate detection logic - Before executing a command, the framework checks if an identical command with the same idempotency key has already been executed
  4. Result consistency - If a duplicate command is detected, the framework returns the cached result instead of re-executing
public class CommandExecutor {
    private final Map<String, CommandResult> idempotencyCache = new ConcurrentHashMap<>();
    private final ScheduledExecutorService cacheCleanup = Executors.newScheduledThreadPool(1);
    
    public CommandResult execute(Command command) {
        String idempotencyKey = command.getIdempotencyKey();
        
        if (idempotencyKey != null) {
            CommandResult cached = idempotencyCache.get(idempotencyKey);
            if (cached != null) {
                return cached;
            }
        }
        
        CommandResult result = executeCommand(command);
        
        if (idempotencyKey != null) {
            idempotencyCache.put(idempotencyKey, result);
            
            // Schedule cleanup after TTL
            cacheCleanup.schedule(
                () -> idempotencyCache.remove(idempotencyKey), 
                IDEMPOTENCY_TTL, TimeUnit.SECONDS
            );
        }
        
        return result;
    }
}

6. Sequence Diagrams for Key Flows

sequenceDiagram
    participant Client
    participant CommandFactory
    participant Command
    participant CommandExecutor
    participant Target

    Client->>CommandFactory: Create command with parameters
    CommandFactory-->>Client: Return concrete Command instance
    Client->>CommandExecutor: Execute(command)
    CommandExecutor->>Command: execute()
    Command->>Target: invoke(target, params)
    Target-->>Command: return result
    Command-->>CommandExecutor: return result
    CommandExecutor-->>Client: return final result
sequenceDiagram
    participant Client
    participant CommandFactory
    participant Command
    participant CommandExecutor
    participant Target
    participant ErrorHandler

    Client->>CommandFactory: Create command with parameters
    CommandFactory-->>Client: Return concrete Command instance
    Client->>CommandExecutor: Execute(command)
    CommandExecutor->>Command: execute()
    Command->>Target: invoke(target, params)
    Target-->>Command: throw exception
    Command->>ErrorHandler: Handle error
    ErrorHandler-->>CommandExecutor: Return error response
    CommandExecutor-->>Client: Return error result
sequenceDiagram
    participant Client
    participant CommandFactory
    participant Command
    participant AsyncCommandExecutor
    participant EventPublisher
    participant MessageQueue

    Client->>CommandFactory: Create async command with parameters
    CommandFactory-->>Client: Return concrete AsyncCommand instance
    Client->>AsyncCommandExecutor: Execute(command)
    AsyncCommandExecutor->>Command: execute()
    Command->>Target: invoke(target, params)
    Target-->>Command: return result
    Command->>EventPublisher: Publish event
    EventPublisher->>MessageQueue: Send to queue
    MessageQueue-->>EventPublisher: Acknowledge
    EventPublisher-->>AsyncCommandExecutor: Event published successfully
    AsyncCommandExecutor-->>Client: Return async response

7. Caching Design

Cache Strategy Selection

For a Command pattern framework, we'll use cache-aside strategy as it provides the most flexibility and control over cache operations. This approach allows explicit cache management, enabling fine-grained control over when and how data is cached or invalidated.

Cache Key Design

Cache keys will follow a structured namespace pattern:

{namespace}:{entityType}:{entityId}:{commandType}:{version}

Example key structure:

command:order:12345:placeOrder:v1

TTL Strategy

Data TypeTTLReason
Command metadata1 hourCommands are typically processed quickly, but metadata may need to be retained
Command execution results5 minutesRecent command outcomes should be available for quick access
Command state transitions30 secondsState changes are frequent and should not persist too long
Command dependencies1 hourDependencies like configuration or policy data should be reasonably fresh

Cache Eviction Policy

We'll implement LRU (Least Recently Used) eviction policy for most cache entries with TTL fallback. This ensures that frequently accessed command patterns remain in cache while older entries are automatically evicted.

Cache Stampede Prevention

To prevent cache stampedes:

  1. Probabilistic early expiry: Set cache TTL to 75% of actual TTL, allowing for 25% overlap
  2. Distributed locks: Use Redis distributed locks when cache misses occur to prevent multiple concurrent requests from hitting the database
  3. Cache warming: Pre-warm critical command patterns during system startup

Distributed Cache Architecture

We'll use Redis Cluster with:

  • 3 master nodes for high availability
  • 3 replica nodes per master for read scaling
  • Automatic failover with Redis Sentinel monitoring
  • Sharding based on command type and entity ID

Cache Invalidation Strategy

We'll implement event-driven invalidation:

  1. When command execution completes, publish cache invalidation events
  2. Use Redis Pub/Sub to notify all cache instances of invalidations
  3. Implement cache invalidation for specific command types and entities
  4. Support for bulk invalidation when command patterns change

Cache Implementation Example (Java)

@Component
public class CommandCache {
    private final RedisTemplate<String, Object> redisTemplate;
    private final ObjectMapper objectMapper;
    
    public CommandCache(RedisTemplate<String, Object> redisTemplate, ObjectMapper objectMapper) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
    }
    
    public void putCommandResult(String commandId, CommandResult result) {
        String key = buildCacheKey("command", "result", commandId, null);
        redisTemplate.opsForValue().set(key, result, Duration.ofMinutes(5));
    }
    
    public CommandResult getCommandResult(String commandId) {
        String key = buildCacheKey("command", "result", commandId, null);
        return (CommandResult) redisTemplate.opsForValue().get(key);
    }
    
    public void invalidateCommand(String commandId) {
        String key = buildCacheKey("command", "result", commandId, null);
        redisTemplate.delete(key);
    }
    
    private String buildCacheKey(String namespace, String entityType, String entityId, String commandType) {
        return String.format("%s:%s:%s:%s:v1", namespace, entityType, entityId, commandType);
    }
}

8. Repository / Data Access Layer

Repository Pattern Implementation

The repository pattern is implemented to abstract data access logic and provide a clean separation between domain objects and persistence concerns. Each domain entity will have its own repository interface with standardized CRUD operations.

public interface CommandRepository {
    Command save(Command command);
    Optional<Command> findById(UUID id);
    List<Command> findAll();
    void deleteById(UUID id);
    List<Command> findByStatus(CommandStatus status);
    long countByStatus(CommandStatus status);
}

ORM vs. Raw Query Decisions

For the command framework, we use JPA/Hibernate as the ORM for most operations due to:

  • Type safety and compile-time validation
  • Reduced boilerplate code
  • Support for complex queries with JPQL/HQL
  • Built-in lazy loading and caching capabilities

Raw SQL is used for:

  • High-performance batch operations
  • Complex analytics queries that benefit from database-specific optimizations
  • Performance-critical operations where ORM overhead is unacceptable

Read vs. Write Separation

We implement CQRS (Command Query Responsibility Segregation) for the command framework:

public interface CommandReadRepository {
    Optional<Command> findById(UUID id);
    List<Command> findByStatus(CommandStatus status);
    Page<Command> findAll(Pageable pageable);
}

public interface CommandWriteRepository {
    Command save(Command command);
    void deleteById(UUID id);
}

Connection Pool Configuration

Connection pool is configured with:

  • Pool size: 20-50 connections (based on concurrent request load)
  • Timeout: 30 seconds for connection acquisition
  • Validation query: SELECT 1 to verify connection health
  • Max lifetime: 30 minutes to prevent memory leaks
@Configuration
public class DatabaseConfig {
    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:postgresql://localhost:5432/command_framework");
        dataSource.setMaximumPoolSize(30);
        dataSource.setConnectionTimeout(30000);
        dataSource.setIdleTimeout(600000);
        dataSource.setMaxLifetime(1800000);
        dataSource.setValidationTimeout(5000);
        dataSource.setLeakDetectionThreshold(60000);
        return dataSource;
    }
}

N+1 Query Prevention Strategy

We implement eager loading for frequently accessed relationships and use batch fetching:

@Entity
@Table(name = "commands")
public class Command {
    @Id
    private UUID id;
    
    @OneToMany(mappedBy = "command", fetch = FetchType.LAZY)
    @BatchSize(size = 20)
    private List<CommandExecution> executions;
    
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id")
    private User owner;
}

Batch Operation Design

Batch operations are implemented for high-volume data processing:

@Repository
public class CommandBatchRepository {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void batchSave(List<Command> commands) {
        int batchSize = 1000;
        for (int i = 0; i < commands.size(); i += batchSize) {
            List<Command> batch = commands.subList(i, 
                Math.min(i + batchSize, commands.size()));
            
            String sql = "INSERT INTO commands (id, name, status, created_at) VALUES ";
            // Build batch insert with parameter placeholders
            // Execute using native query for better performance
            
            entityManager.flush();
            entityManager.clear();
        }
    }
}

Transaction Management

Repository methods are wrapped in transactional boundaries:

@Repository
@Transactional
public class CommandRepositoryImpl implements CommandRepository {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public Command save(Command command) {
        if (command.getId() == null) {
            entityManager.persist(command);
        } else {
            entityManager.merge(command);
        }
        return command;
    }
    
    @Override
    @Transactional(readOnly = true)
    public Optional<Command> findById(UUID id) {
        return Optional.ofNullable(entityManager.find(Command.class, id));
    }
}

Query Optimization

We implement:

  • Composite indexes on frequently queried fields
  • Partial indexes for filtered queries (e.g., only active commands)
  • Covering indexes to avoid table lookups
-- Composite index for status and creation time
CREATE INDEX idx_commands_status_created ON commands(status, created_at);

-- Partial index for active commands only
CREATE INDEX idx_commands_active ON commands(id) WHERE status = 'ACTIVE';

-- Covering index for common read queries
CREATE INDEX idx_commands_covering ON commands(id, status, name, created_at);

Data Access Layer Testing

Repository methods are tested with:

  • Embedded H2 database for unit tests
  • Testcontainers for integration testing with real PostgreSQL
  • Mocking of external dependencies in unit tests
@ExtendWith(MockitoExtension.class)
class CommandRepositoryTest {
    
    @Mock
    private EntityManager entityManager;
    
    @InjectMocks
    private CommandRepositoryImpl repository;
    
    @Test
    void findById_shouldReturnCommand() {
        // Given
        Command command = new Command();
        when(entityManager.find(Command.class, UUID.randomUUID()))
            .thenReturn(command);
            
        // When
        Optional<Command> result = repository.findById(UUID.randomUUID());
        
        // Then
        assertThat(result).isPresent();
    }
}

9. Asynchronous Processing & Messaging

Event-driven boundaries in this feature

The Command pattern framework is primarily synchronous by design, but certain commands may benefit from asynchronous execution to improve system responsiveness and throughput. The framework will support asynchronous command execution for long-running operations or commands that can be safely decoupled from the request lifecycle.

Synchronous boundaries:

  • Immediate command execution (e.g., validation, simple CRUD operations)
  • Commands that must complete before returning a response to the client

Asynchronous boundaries:

  • Long-running batch operations (e.g., data migration, report generation)
  • Commands that can be queued for later processing (e.g., sending notifications, triggering analytics)
  • Commands that are idempotent and can tolerate eventual consistency

Message schema design with sample payload

The framework will support a standardized event format using JSON Schema for consistency and versioning:

{
  "version": "1.0",
  "eventId": "uuid",
  "eventType": "command.executed",
  "timestamp": "2023-10-25T10:30:00Z",
  "commandId": "uuid",
  "commandType": "UserCreationCommand",
  "payload": {
    "userId": "1234567890",
    "email": "user@example.com"
  },
  "metadata": {
    "source": "command-framework",
    "correlationId": "uuid"
  }
}

Dead letter queue strategy and poison message handling

The framework will implement a robust DLQ strategy to handle failed command processing:

  1. Dead Letter Queue Configuration:

    • Messages that fail after maximum retry attempts (5) will be moved to DLQ
    • DLQ messages are stored with metadata indicating the original queue, error type, and failure count
    • DLQ messages are retained for 7 days before permanent deletion
  2. Poison Message Handling:

    • Messages that consistently fail (same error type, same command) are flagged as poison
    • Poison messages are automatically routed to a separate "poison" queue for manual inspection
    • A monitoring alert is triggered when poison message thresholds are exceeded (e.g., 10+ messages in 5 minutes)

At-least-once vs. exactly-once delivery

The framework will implement at-least-once delivery with idempotency support:

  1. Delivery Strategy:

    • Messages are published to a message broker (e.g., Kafka or RabbitMQ) with guaranteed delivery
    • Consumers acknowledge receipt and process messages once
  2. Idempotency Implementation:

    • Each command includes a unique commandId that serves as an idempotency key
    • Command processors maintain a cache of processed command IDs to prevent duplicate execution
    • Idempotency cache uses Redis with TTL of 24 hours to prevent memory leaks
  3. Justification:

    • At-least-once delivery is preferred for command processing where duplicate execution could be handled gracefully
    • Idempotency ensures system consistency without requiring complex transactional guarantees

Retry policy for consumers

The framework implements a retry strategy with exponential backoff:

public class CommandRetryPolicy {
    private static final int MAX_RETRIES = 5;
    private static final long INITIAL_DELAY_MS = 1000;
    private static final double BACKOFF_MULTIPLIER = 2.0;
    private static final double JITTER_RANGE = 0.1; // 10% jitter
    
    public static long calculateDelay(int attempt) {
        if (attempt >= MAX_RETRIES) {
            return -1; // No more retries
        }
        
        long baseDelay = (long) (INITIAL_DELAY_MS * Math.pow(BACKOFF_MULTIPLIER, attempt));
        long jitter = (long) (baseDelay * JITTER_RANGE * (Math.random() - 0.5) * 2);
        return baseDelay + jitter;
    }
    
    public static boolean shouldRetry(int attempt, Exception error) {
        // Don't retry on certain non-recoverable errors
        return attempt < MAX_RETRIES && !isNonRetryableError(error);
    }
    
    private static boolean isNonRetryableError(Exception error) {
        return error instanceof IllegalArgumentException || 
               error instanceof ValidationException;
    }
}

Outbox pattern for reliable event publishing

The framework will implement the outbox pattern to ensure command execution and event publishing are atomic:

@Component
public class CommandOutboxService {
    private final CommandEventRepository commandEventRepository;
    private final EventPublisher eventPublisher;
    
    @Transactional
    public void executeCommandWithEvent(Command command) {
        // Execute the command
        CommandResult result = command.execute();
        
        // Store event in outbox before committing transaction
        CommandEvent event = new CommandEvent(
            command.getId(),
            command.getType(),
            result.getPayload(),
            "command.executed"
        );
        
        commandEventRepository.save(event);
        
        // Publish event after successful transaction commit
        // This ensures that if the transaction rolls back, 
        // the event is never published
    }
    
    @EventListener
    public void handleTransactionCommit(TransactionEvent event) {
        // Publish all pending events from outbox
        List<CommandEvent> pendingEvents = commandEventRepository.findPending();
        for (CommandEvent evt : pendingEvents) {
            eventPublisher.publish(evt);
            evt.markAsPublished();
        }
    }
}

Sequence diagram for async flows

sequenceDiagram
    participant Client
    participant CommandFramework
    participant MessageBroker
    participant CommandProcessor
    
    Client->>CommandFramework: Submit Async Command (e.g., UserCreationCommand)
    CommandFramework->>CommandFramework: Validate Command
    CommandFramework->>MessageBroker: Publish Command to Queue
    MessageBroker->>CommandProcessor: Deliver Command
    CommandProcessor->>CommandProcessor: Process Command (Async)
    CommandProcessor->>MessageBroker: Publish Event to Event Stream
    MessageBroker-->>CommandProcessor: Acknowledge Event Publishing
    CommandProcessor->>CommandProcessor: Update Command Status (Completed)
    CommandProcessor->>CommandFramework: Notify Completion
    CommandFramework-->>Client: Return Async Response (Command ID)
    
    Note over Client,CommandFramework: Client can poll for command status or receive callback

The async flow ensures that:

  1. Command submission is immediate and non-blocking for the client
  2. Commands are processed in a separate thread pool to avoid blocking the main request processing
  3. Events are published only after successful command execution, maintaining consistency
  4. The system can handle high-volume command processing without impacting request latency

10. Error Handling Design

Error Classification Hierarchy

Errors in the Command pattern framework are categorized into three primary levels:

  1. Domain Errors - Business logic violations (e.g., invalid command parameters, state constraints)
  2. Infrastructure Errors - System-level issues (e.g., database connection failures, external service timeouts)
  3. Validation Errors - Input validation failures (e.g., missing required fields, malformed data)
public abstract class CommandException extends RuntimeException {
    private final ErrorCode errorCode;
    
    public CommandException(ErrorCode errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public CommandException(ErrorCode errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }
    
    public ErrorCode getErrorCode() {
        return errorCode;
    }
}

public class DomainCommandException extends CommandException {
    public DomainCommandException(ErrorCode errorCode, String message) {
        super(errorCode, message);
    }
    
    public DomainCommandException(ErrorCode errorCode, String message, Throwable cause) {
        super(errorCode, message, cause);
    }
}

public class InfrastructureCommandException extends CommandException {
    public InfrastructureCommandException(ErrorCode errorCode, String message) {
        super(errorCode, message);
    }
    
    public InfrastructureCommandException(ErrorCode errorCode, String message, Throwable cause) {
        super(errorCode, message, cause);
    }
}

public class ValidationCommandException extends CommandException {
    public ValidationCommandException(ErrorCode errorCode, String message) {
        super(errorCode, message);
    }
    
    public ValidationCommandException(ErrorCode errorCode, String message, Throwable cause) {
        super(errorCode, message, cause);
    }
}

Error Propagation Strategy

The framework follows a layered error propagation model:

  1. Local handling - Command handlers catch and wrap domain-specific errors
  2. Service boundary - Errors are re-thrown at service layer to allow upstream aggregation
  3. Global handling - HTTP layer converts to appropriate response codes

Retry Strategy with Exponential Backoff and Jitter

For transient infrastructure errors, the framework implements a retry mechanism:

public class RetryConfig {
    private final int maxAttempts;
    private final long initialDelayMs;
    private final double multiplier;
    private final long maxDelayMs;
    
    public RetryConfig(int maxAttempts, long initialDelayMs, double multiplier, long maxDelayMs) {
        this.maxAttempts = maxAttempts;
        this.initialDelayMs = initialDelayMs;
        this.multiplier = multiplier;
        this.maxDelayMs = maxDelayMs;
    }
    
    public long getDelay(int attempt) {
        long delay = (long) (initialDelayMs * Math.pow(multiplier, attempt - 1));
        return Math.min(delay, maxDelayMs);
    }
}

public class RetryableCommandExecutor {
    private final RetryConfig retryConfig;
    
    public <T> T executeWithRetry(Supplier<T> operation, Predicate<Throwable> retryable) {
        int attempt = 0;
        Throwable lastFailure = null;
        
        while (attempt < retryConfig.getMaxAttempts()) {
            try {
                return operation.get();
            } catch (Throwable t) {
                lastFailure = t;
                if (!retryable.test(t) || attempt >= retryConfig.getMaxAttempts() - 1) {
                    throw new InfrastructureCommandException(
                        ErrorCode.COMMAND_EXECUTION_FAILED, 
                        "Command execution failed after " + retryConfig.getMaxAttempts() + " attempts", 
                        lastFailure);
                }
                
                long delay = retryConfig.getDelay(attempt + 1);
                // Add jitter (±25%)
                long jitter = (long) (delay * 0.25 * (Math.random() - 0.5) * 2);
                try {
                    Thread.sleep(delay + jitter);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new InfrastructureCommandException(
                        ErrorCode.COMMAND_EXECUTION_FAILED, 
                        "Retry interrupted", 
                        ie);
                }
            }
        }
        
        throw new InfrastructureCommandException(
            ErrorCode.COMMAND_EXECUTION_FAILED, 
            "Command execution failed after " + retryConfig.getMaxAttempts() + " attempts", 
            lastFailure);
    }
}

Circuit Breaker Design

For external dependencies, the circuit breaker pattern prevents cascading failures:

public class CircuitBreaker {
    private final int failureThreshold;
    private final long timeoutMs;
    private final int successThreshold;
    
    private volatile CircuitState state = CircuitState.CLOSED;
    private volatile int failureCount = 0;
    private volatile long lastFailureTime = 0;
    
    public enum CircuitState {
        CLOSED, OPEN, HALF_OPEN
    }
    
    public boolean canExecute() {
        switch (state) {
            case CLOSED:
                return true;
            case HALF_OPEN:
                return failureCount < successThreshold;
            case OPEN:
                if (System.currentTimeMillis() - lastFailureTime > timeoutMs) {
                    state = CircuitState.HALF_OPEN;
                    return true;
                }
                return false;
            default:
                throw new IllegalStateException("Unknown circuit state: " + state);
        }
    }
    
    public void recordSuccess() {
        if (state == CircuitState.HALF_OPEN) {
            failureCount = 0;
            state = CircuitState.CLOSED;
        }
    }
    
    public void recordFailure() {
        lastFailureTime = System.currentTimeMillis();
        
        switch (state) {
            case CLOSED:
                failureCount++;
                if (failureCount >= failureThreshold) {
                    state = CircuitState.OPEN;
                }
                break;
            case HALF_OPEN:
                failureCount++;
                if (failureCount >= failureThreshold) {
                    state = CircuitState.OPEN;
                } else {
                    state = CircuitState.CLOSED;
                }
                break;
            case OPEN:
                // Reset failure count on timeout
                if (System.currentTimeMillis() - lastFailureTime > timeoutMs) {
                    state = CircuitState.HALF_OPEN;
                }
                break;
        }
    }
}

Fallback Behavior Design

Fallbacks are implemented for critical external dependencies:

public class CommandWithFallback<T> {
    private final Supplier<T> primaryOperation;
    private final Supplier<T> fallbackOperation;
    private final CircuitBreaker circuitBreaker;
    
    public CommandWithFallback(Supplier<T> primaryOperation, 
                              Supplier<T> fallbackOperation,
                              CircuitBreaker circuitBreaker) {
        this.primaryOperation = primaryOperation;
        this.fallbackOperation = fallbackOperation;
        this.circuitBreaker = circuitBreaker;
    }
    
    public T execute() {
        if (circuitBreaker.canExecute()) {
            try {
                T result = primaryOperation.get();
                circuitBreaker.recordSuccess();
                return result;
            } catch (Exception e) {
                circuitBreaker.recordFailure();
                // Fallback logic
                if (fallbackOperation != null) {
                    return fallbackOperation.get();
                }
                throw e;
            }
        } else {
            // Circuit is open, use fallback or throw error
            if (fallbackOperation != null) {
                return fallbackOperation.get();
            }
            throw new InfrastructureCommandException(
                ErrorCode.CIRCUIT_OPEN,
                "External dependency circuit is open, fallback not available");
        }
    }
}

Error Logging Format and Alerting Thresholds

Structured logging includes trace context and error metadata:

public class CommandErrorLogger {
    private static final Logger logger = LoggerFactory.getLogger(CommandErrorLogger.class);
    
    public void logCommandError(String commandType, String userId, String traceId, 
                               ErrorCode errorCode, Throwable error) {
        Map<String, Object> logFields = new HashMap<>();
        logFields.put("timestamp", Instant.now().toString());
        logFields.put("trace_id", traceId);
        logFields.put("user_id", userId);
        logFields.put("command_type", commandType);
        logFields.put("error_code", errorCode.getCode());
        logFields.put("error_message", error.getMessage());
        logFields.put("error_stack_trace", error.getStackTrace().toString());
        
        logger.error("Command execution failed", 
            new ObjectMapper().valueToTree(logFields));
        
        // Alerting logic based on error severity
        if (errorCode.getSeverity() == ErrorSeverity.CRITICAL) {
            // Send alert to monitoring system
            sendAlert(errorCode, commandType);
        }
    }
    
    private void sendAlert(ErrorCode errorCode, String commandType) {
        // Implementation for alerting system integration
    }
}

Error Code Definitions

public enum ErrorCode {
    COMMAND_VALIDATION_FAILED(1001, ErrorSeverity.HIGH),
    COMMAND_EXECUTION_FAILED(1002, ErrorSeverity.MEDIUM),
    CIRCUIT_OPEN(1003, ErrorSeverity.HIGH),
    COMMAND_NOT_FOUND(1004, ErrorSeverity.LOW),
    INVALID_COMMAND_STATE(1005, ErrorSeverity.HIGH),
    DATABASE_CONNECTION_FAILED(2001, ErrorSeverity.CRITICAL),
    EXTERNAL_SERVICE_TIMEOUT(2002, ErrorSeverity.MEDIUM);
    
    private final int code;
    private final ErrorSeverity severity;
    
    ErrorCode(int code, ErrorSeverity severity) {
        this.code = code;
        this.severity = severity;
    }
    
    public int getCode() {
        return code;
    }
    
    public ErrorSeverity getSeverity() {
        return severity;
    }
}

public enum ErrorSeverity {
    LOW, MEDIUM, HIGH, CRITICAL
}

11. Security Design

Input Validation and Sanitization Strategy

All command inputs undergo strict validation using Apache Commons Validator for structured data and OWASP Java HTML Sanitizer for unstructured text. Custom validation rules enforce field constraints (min/max length, regex patterns, enum values) and sanitize potentially dangerous content like HTML tags, JavaScript, and SQL injection patterns.

SQL Injection Prevention

All database interactions use parameterized prepared statements exclusively. No dynamic query construction or string concatenation is permitted. The framework includes a SQL injection detection middleware that logs suspicious patterns and triggers alerts for potential attacks.

Authentication Middleware Design

Authentication is handled via JWT middleware that validates tokens against a configured public key. The middleware extracts user identity and roles from claims, caches validated tokens for 5 minutes using Redis with TTL, and rejects expired or malformed tokens with 401 responses.

Authorization Enforcement

Role-based access control (RBAC) is implemented at the service layer using a permission-checking decorator. Each command has associated roles (e.g., ADMIN, USER) defined in a configuration file. The authorization system supports hierarchical role inheritance and checks permissions before command execution.

Sensitive Data Encryption

All sensitive fields (user passwords, personal identifiers) are encrypted at rest using AES-256-GCM with keys rotated every 90 days. In-transit encryption is enforced via TLS 1.3 for all API endpoints and database connections using mutual TLS.

PII Handling and Masking Strategy

PII fields (email, phone, SSN) are masked in logs using a configurable pattern (e.g., "user@example.com" → "u***@example.com"). The framework includes a PII tracking system that logs data access and automatically redacts sensitive fields from all response payloads.

Secrets Management

Configuration secrets (database passwords, API keys) are managed through AWS Secrets Manager with automatic rotation. The framework uses a secrets cache with 15-minute TTL to minimize API calls while ensuring freshness of credentials.

Audit Logging Design

All command execution events are logged to a structured audit trail including:

  • Timestamp and user identity (trace_id, span_id)
  • Command name and parameters (with PII masked)
  • Execution status (success/failure)
  • Response time and error details
  • IP address and user agent for security monitoring

Audit logs are stored in a separate encrypted database with 30-day retention policy and automatically exported to SIEM for compliance monitoring.

12. Testing Strategy

Unit Testing Scope and Coverage Targets (%)

  • Core Command Framework: 90% code coverage with 100% branch coverage for command execution, registration, and lifecycle management
  • Command Implementations: 85% coverage for business logic in concrete command classes, focusing on edge cases and error paths
  • State Management: 95% coverage for state transitions, validation logic, and rollback scenarios
  • Dependency Injection: 80% coverage for service layer integration points and mockable interfaces
  • Error Handling: 100% coverage for all error paths, including validation failures, execution exceptions, and recovery scenarios

Integration Testing Strategy

  • Command Registry: Test with 100+ concurrent command registrations to verify thread safety and uniqueness constraints
  • Command Execution Pipeline: Validate end-to-end execution through command bus, including pre/post-processing hooks and error propagation
  • Database Integration: Use embedded H2 database to test command persistence, rollback behavior, and state consistency across transactions
  • External Service Mocking: Implement WireMock or Mockito for external service dependencies (e.g., payment gateways, notification services)
  • Event Publishing: Validate event-driven command completion through embedded message broker (e.g., In-memory Kafka or Redis Streams)

Contract Testing

  • API Contract Verification: Use Pact for contract testing between command framework and consumer services, ensuring consistent payload formats and error responses
  • Event Schema Contracts: Define Avro schemas for command execution events with versioning strategy to prevent breaking changes
  • Consumer-Provider Pacts: Establish pacts between command framework and downstream consumers to validate message contract compliance

Performance Testing Tool and Key Scenarios

  • Load Testing: Use Gatling for simulating concurrent command execution (1000+ concurrent commands) with 50% read-heavy and 50% write-heavy workloads
  • Throughput Testing: Measure commands per second under varying load conditions (100, 500, 1000 concurrent users)
  • Latency Testing: Target p95 latency under 200ms for command execution, with p99 under 500ms
  • Memory Usage: Monitor heap usage and GC pressure during high-concurrency command execution (10k+ commands/sec)
  • Stress Testing: Simulate burst load scenarios (5000 concurrent commands) to validate system resilience and recovery behavior

Test Data Management Strategy

  • Test Fixtures: Use JSON/YAML fixtures for command configurations, expected outcomes, and state transitions
  • Factory Patterns: Implement factory classes to generate valid command objects with randomized parameters for stress testing
  • Database Seeding: Use Liquibase or Flyway migrations to seed test data with realistic command execution histories and state snapshots
  • Test Data Isolation: Use database transaction isolation or separate test databases to prevent cross-test contamination
  • Data Generation Libraries: Leverage libraries like Faker for generating realistic command payloads and user contexts
// Example unit test for Command Framework with Mockito
@Test
public void testCommandExecutionWithSuccess() {
    // Given
    Command command = mock(Command.class);
    when(command.execute()).thenReturn(CommandResult.success("Operation completed"));
    
    // When
    CommandResult result = command.execute();
    
    // Then
    assertThat(result.isSuccess()).isTrue();
    assertThat(result.getMessage()).isEqualTo("Operation completed");
}

13. Observability Design

Structured Logging Schema

The logging system will use structured JSON format to ensure consistent parsing and filtering across services. Each log entry will include the following required fields:

{
  "timestamp": "2023-10-27T14:30:45.123Z",
  "level": "INFO|WARN|ERROR",
  "trace_id": "12345678-1234-1234-1234-123456789012",
  "span_id": "abcdef12-3456-7890-abcd-ef1234567890",
  "service_name": "command-framework-service",
  "user_id": "user-12345",
  "command_id": "cmd-98765",
  "operation": "executeCommand",
  "message": "Command executed successfully",
  "error_code": "COMMAND_EXECUTION_SUCCESS",
  "duration_ms": 150,
  "error_details": {
    "message": "Command failed due to validation error",
    "stack_trace": "...",
    "cause": "VALIDATION_ERROR"
  }
}

PII masking rules will be applied to sensitive fields like user_id and command_id in logs by truncating or hashing values when they appear in error contexts.

Distributed Tracing Instrumentation

We'll use OpenTelemetry for distributed tracing with custom spans to capture command execution lifecycle:

  1. Command Execution Span: Root span for each command execution
    • Attributes: command_type, command_id, user_id, execution_status
  2. Command Validation Span: Sub-span for validation logic
    • Attributes: validation_rules, validation_result
  3. Command Execution Span: Sub-span for actual command execution
    • Attributes: execution_duration, resource_usage

Example instrumentation in Java:

@OpenTelemetry
public CommandResult executeCommand(Command command) {
    Span span = tracer.spanBuilder("executeCommand")
        .setAttribute("command_type", command.getType())
        .setAttribute("command_id", command.getId())
        .startSpan();
    
    try {
        // Validation logic
        Span validationSpan = tracer.spanBuilder("validateCommand")
            .spanContext(span.context())
            .startSpan();
        validateCommand(command);
        validationSpan.end();
        
        // Execution logic
        Span executionSpan = tracer.spanBuilder("executeCommand")
            .spanContext(span.context())
            .startSpan();
        CommandResult result = executeCommandInternal(command);
        executionSpan.end();
        
        span.setAttribute("execution_status", "SUCCESS");
        return result;
    } catch (Exception e) {
        span.setAttribute("execution_status", "FAILURE");
        span.recordException(e);
        throw e;
    } finally {
        span.end();
    }
}

Metrics Design

Key RED metrics will be collected at service level:

Metric NameTypeLabelsDescription
command_execution_countCountercommand_type, status, user_idTotal commands executed
command_execution_durationHistogramcommand_type, statusTime to execute commands (ms)
command_validation_errorsCountercommand_type, error_typeValidation failures
command_execution_failuresCountercommand_type, failure_reasonExecution failures

Example histogram configuration:

Histogram commandDuration = Histogram.builder()
    .name("command_execution_duration")
    .description("Duration of command executions in milliseconds")
    .baseUnit("milliseconds")
    .build();

Health Check Endpoint Design

Three health check endpoints will be implemented:

  1. /health - Application liveness

    • Checks database connectivity, cache availability, core services
    • Returns 200 OK when all dependencies healthy
  2. /ready - Service readiness

    • Validates that service can accept traffic (queue consumers ready, DB connections established)
    • Returns 200 OK when ready for traffic
  3. /live - System status

    • Reports overall system health including external dependencies
    • Returns 503 Service Unavailable when critical components down

Alerting Thresholds

Critical alert thresholds:

MetricWarning LevelCritical LevelEscalation
command_execution_duration p99> 500ms> 1000msOn-call engineer
command_execution_failures rate> 10 per minute> 50 per minuteOn-call engineer
command_validation_errors rate> 5 per minute> 20 per minuteOn-call engineer
system_cpu_utilization> 80%> 90%On-call engineer
memory_usage> 85%> 95%On-call engineer

Alerts will be sent via PagerDuty with detailed context including trace IDs and error patterns for rapid incident response.

14. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering alignment

  • Command registration strategy: Should commands be auto-discovered via reflection or explicitly registered? Auto-discovery simplifies configuration but can lead to security concerns and performance overhead during startup.
  • Command execution context: Should commands execute in the same transaction as their caller, or should they be isolated? Transaction isolation affects data consistency guarantees and can impact performance.
  • Command chaining vs. composition: Should the framework support chaining commands (e.g., commandA -> commandB -> commandC) or composition (e.g., CommandA implements Command with execute() { commandB.execute(); commandC.execute(); })? Chaining provides better orchestration but adds complexity to error handling and rollback.

Known trade-offs and justifications

Trade-offOption 1Option 2Justification
Flexibility vs. SimplicityGeneric command interface with dynamic parametersCommand-specific interfaces with typed parametersTyped interfaces provide better IDE support and compile-time safety at the cost of reduced flexibility for generic command processing
Performance vs. ExtensibilityCommand pre-compilation and cachingRuntime command discovery and instantiationPre-compilation improves performance but reduces runtime extensibility
Error Handling GranularitySingle global error handlerPer-command error handlersGlobal handlers simplify configuration but reduce flexibility in handling specific command errors
Transaction BoundariesCommands execute in parent transactionCommands execute in isolated transactionIsolated transactions provide better error recovery but increase complexity and resource usage

Risks and mitigation strategies

  • Security Risk: Command injection through dynamic command execution. Mitigation: Implement strict command whitelisting and input validation.
  • Performance Risk: Command execution overhead from reflection or dynamic dispatch. Mitigation: Cache command instances and use efficient dispatch mechanisms.
  • Maintainability Risk: Complex command composition leading to hard-to-debug systems. Mitigation: Enforce clear separation of concerns and provide comprehensive logging.
  • Concurrency Risk: Race conditions in command execution when commands modify shared state. Mitigation: Implement proper locking and atomic operations where necessary.
  • Audit Risk: Inability to track command execution history for compliance. Mitigation: Implement comprehensive audit logging and command metadata tracking.

Technical debt acknowledged

  • Command Metadata Management: Current design doesn't provide a robust way to version command metadata or handle backward compatibility for command signatures. This could lead to breaking changes in the future.
  • Testing Complexity: The framework's flexibility makes unit testing of individual commands challenging without proper abstraction layers.
  • Monitoring Gaps: While basic logging is provided, deeper observability (e.g., command execution metrics) requires additional integration with monitoring systems.
  • Command Lifecycle Management: There's no built-in mechanism for command lifecycle hooks (e.g., pre-execution, post-execution) that could be useful for cross-cutting concerns like metrics collection or audit logging.
  • Type Safety Limitations: The generic command interface sacrifices type safety for flexibility, which could lead to runtime errors that would otherwise be caught at compile time.

Was this article helpful?

Topics in this article

More from Backend LLD

  • Backend LLD

    State Machine Design Patterns and Implementation

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

  • Backend LLD

    Observer-Based Event System Design

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

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.