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
- What is the expected scale of command execution? Are we designing for thousands of commands per second or more?
- What consistency requirements are there? Should all command executions be strongly consistent, or can eventual consistency be acceptable?
- What are the key entities in the command execution lifecycle? Is there a need to track command state, status, or audit logs?
- What is the read/write ratio for command metadata? Will there be frequent reads of command history or status?
- How sensitive is the system to latency? Are there time-critical commands that must complete within specific SLAs?
- What authentication and authorization model is expected? Should commands be restricted to specific roles or users?
- 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:
- Strong typing: gRPC's protobuf definitions provide compile-time type safety, which is crucial for a framework that needs to handle diverse command types
- Performance: Binary protocol with efficient serialization is ideal for command execution workloads
- Built-in support for streaming: Enables real-time feedback and progress tracking for long-running commands
- Language agnostic: Supports multiple client languages (Java, Go, Python, etc.) for framework extensibility
- Service contracts: Clear separation between command interface and implementation
Endpoint Definitions
| Method | Path | Description | Auth |
|---|---|---|---|
| ExecuteCommand | /command.v1.CommandService/ExecuteCommand | Executes a single command with payload and metadata | JWT/OAuth2 |
| BatchExecuteCommands | /command.v1.CommandService/BatchExecuteCommands | Executes multiple commands in batch mode | JWT/OAuth2 |
| GetCommandStatus | /command.v1.CommandService/GetCommandStatus | Retrieves status and result of a previously executed command | JWT/OAuth2 |
| CancelCommand | /command.v1.CommandService/CancelCommand | Requests cancellation of a running command | JWT/OAuth2 |
| ListCommands | /command.v1.CommandService/ListCommands | Lists commands with filtering and pagination | JWT/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:
- Default parameter values for new optional fields
- Migration paths for breaking changes
- Deprecation warnings for older versions
Pagination Design
Cursor-based pagination will be used for the ListCommands endpoint because:
- Stability: Cursor-based pagination is not affected by data changes during pagination
- Consistency: Provides consistent results even when items are inserted/deleted between requests
- 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 commandscommand_admin- Can cancel, view all commandscommand_reader- Can only read command status
Rate Limiting Strategy
Rate limiting will be implemented per endpoint:
| Endpoint | Limit | Window | Enforcement |
|---|---|---|---|
| ExecuteCommand | 1000 req/min | 1 minute | In-memory + Redis |
| BatchExecuteCommands | 500 req/min | 1 minute | In-memory + Redis |
| GetCommandStatus | 5000 req/min | 1 minute | In-memory + Redis |
| CancelCommand | 200 req/min | 1 minute | In-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:
- Clients provide optional
idempotency_keyin ExecuteCommandRequest - Server uses this key to detect duplicate requests within 5 minutes
- 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 nameidx_command_executions_command_id- Composite on command_id for fast execution history lookupsidx_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 statusidx_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 infoidx_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
CANCELLEDrather 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_byfield captures the operator identitycreated_atandupdated_attimestamps capture timingstatusfield tracks state transitionsmetadatafield 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:
- CommandProcessor: Main orchestrator that handles command execution lifecycle, including validation, execution, rollback, and audit logging.
- CommandHandler: Interface for implementing command-specific business logic.
- CommandContext: Immutable context container for command execution metadata.
- CommandFactory: Factory for creating command instances from serialized payloads.
- 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:
- Single command execution - Each command operation is atomic within a single transaction scope.
- Command handler isolation - Each command handler operates in its own transaction context.
- 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:
- Immutable command objects - Command instances are read-only once created, preventing concurrent modification
- Synchronized execution context - Command execution is wrapped in synchronized blocks where state changes occur
- Atomic operations for shared mutable state using
java.util.concurrent.atomicpackages - 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:
- Fixed ordering of resource acquisition - Commands always acquire locks in a consistent global order
- Timeout-based lock acquisition - Locks are acquired with timeout to prevent indefinite blocking
- Command dependency graph analysis - Framework prevents circular dependencies in command execution order
- 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:
- Idempotency keys - Each command can include an optional idempotency key that uniquely identifies the request
- Command result caching - Results of executed commands are cached with idempotency key as the cache key
- Duplicate detection logic - Before executing a command, the framework checks if an identical command with the same idempotency key has already been executed
- 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 Type | TTL | Reason |
|---|---|---|
| Command metadata | 1 hour | Commands are typically processed quickly, but metadata may need to be retained |
| Command execution results | 5 minutes | Recent command outcomes should be available for quick access |
| Command state transitions | 30 seconds | State changes are frequent and should not persist too long |
| Command dependencies | 1 hour | Dependencies 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:
- Probabilistic early expiry: Set cache TTL to 75% of actual TTL, allowing for 25% overlap
- Distributed locks: Use Redis distributed locks when cache misses occur to prevent multiple concurrent requests from hitting the database
- 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:
- When command execution completes, publish cache invalidation events
- Use Redis Pub/Sub to notify all cache instances of invalidations
- Implement cache invalidation for specific command types and entities
- 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 1to 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:
-
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
-
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:
-
Delivery Strategy:
- Messages are published to a message broker (e.g., Kafka or RabbitMQ) with guaranteed delivery
- Consumers acknowledge receipt and process messages once
-
Idempotency Implementation:
- Each command includes a unique
commandIdthat 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
- Each command includes a unique
-
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:
- Command submission is immediate and non-blocking for the client
- Commands are processed in a separate thread pool to avoid blocking the main request processing
- Events are published only after successful command execution, maintaining consistency
- 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:
- Domain Errors - Business logic violations (e.g., invalid command parameters, state constraints)
- Infrastructure Errors - System-level issues (e.g., database connection failures, external service timeouts)
- 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:
- Local handling - Command handlers catch and wrap domain-specific errors
- Service boundary - Errors are re-thrown at service layer to allow upstream aggregation
- 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:
- Command Execution Span: Root span for each command execution
- Attributes:
command_type,command_id,user_id,execution_status
- Attributes:
- Command Validation Span: Sub-span for validation logic
- Attributes:
validation_rules,validation_result
- Attributes:
- Command Execution Span: Sub-span for actual command execution
- Attributes:
execution_duration,resource_usage
- Attributes:
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 Name | Type | Labels | Description |
|---|---|---|---|
command_execution_count | Counter | command_type, status, user_id | Total commands executed |
command_execution_duration | Histogram | command_type, status | Time to execute commands (ms) |
command_validation_errors | Counter | command_type, error_type | Validation failures |
command_execution_failures | Counter | command_type, failure_reason | Execution 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:
-
/health - Application liveness
- Checks database connectivity, cache availability, core services
- Returns 200 OK when all dependencies healthy
-
/ready - Service readiness
- Validates that service can accept traffic (queue consumers ready, DB connections established)
- Returns 200 OK when ready for traffic
-
/live - System status
- Reports overall system health including external dependencies
- Returns 503 Service Unavailable when critical components down
Alerting Thresholds
Critical alert thresholds:
| Metric | Warning Level | Critical Level | Escalation |
|---|---|---|---|
command_execution_duration p99 | > 500ms | > 1000ms | On-call engineer |
command_execution_failures rate | > 10 per minute | > 50 per minute | On-call engineer |
command_validation_errors rate | > 5 per minute | > 20 per minute | On-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-off | Option 1 | Option 2 | Justification |
|---|---|---|---|
| Flexibility vs. Simplicity | Generic command interface with dynamic parameters | Command-specific interfaces with typed parameters | Typed interfaces provide better IDE support and compile-time safety at the cost of reduced flexibility for generic command processing |
| Performance vs. Extensibility | Command pre-compilation and caching | Runtime command discovery and instantiation | Pre-compilation improves performance but reduces runtime extensibility |
| Error Handling Granularity | Single global error handler | Per-command error handlers | Global handlers simplify configuration but reduce flexibility in handling specific command errors |
| Transaction Boundaries | Commands execute in parent transaction | Commands execute in isolated transaction | Isolated 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.