A blocking system serves as a critical infrastructure component for controlling resource access and preventing system overload through intelligent rate limiting and throttling mechanisms across distributed environments.
1. Problem Statement & Requirements
Clarifying questions
- What is the scope of "blocking"? Is it about blocking users, IP addresses, or content from being accessed? Will this system block based on user behavior, content type, or external threat intelligence?
- What is the expected scale? How many concurrent blocking operations are we expecting per second, and how many total blocked entities (users, IPs, content) will there be?
- What consistency requirements exist? Should blocking be immediate (strong consistency) or can there be a small delay (eventual consistency)?
- What is the read/write ratio? Is this system primarily used for checking if an entity is blocked (read-heavy) or for creating/adding new blocks (write-heavy)?
- What are the latency sensitivity requirements? Should blocking decisions happen in <10ms for real-time traffic or is a few hundred milliseconds acceptable?
- What authentication and authorization model does the system expect? Will it be used internally by other services or exposed publicly with API keys?
- What external systems integrate with this blocking system? Are there integrations with threat intelligence feeds, third-party blocking services, or external APIs?
Functional requirements
- Block entities: Ability to block users, IP addresses, content IDs, or other identifiers by type.
- Check blocking status: Query if an entity is currently blocked.
- Unblock entities: Remove a previously added block.
- Bulk operations: Support for batch blocking and unblocking of entities.
- Block expiration: Optionally set TTLs on blocks to auto-unblock after a period.
- Block metadata: Store additional context with each block (reason, source, timestamp).
- Audit logs: Track who created/modified blocks and when.
- Block categories: Support for categorizing blocks (e.g., spam, malicious, content policy).
Non-functional requirements
- Latency targets:
- p50: <5ms for blocking checks
- p95: <20ms for blocking checks
- p99: <50ms for blocking checks
- Throughput:
- 10,000 blocking checks/second (read-heavy)
- 500 block creation/second (write-heavy)
- Availability: 99.9% uptime for blocking service
- Scalability: Support horizontal scaling to handle peak loads up to 10x current capacity
- Data durability: Blocks must survive system restarts and data center failures
Constraints and assumptions
- Assumption: The blocking system will be used in a high-throughput, low-latency environment typical of FAANG systems.
- Constraint: Blocks must be checked in sub-millisecond time for real-time traffic filtering.
- Assumption: The system will be used by multiple services in a microservices architecture.
- Constraint: No external dependencies for core blocking functionality (e.g., no reliance on external threat feeds for basic operation).
- Assumption: The system will be deployed in a multi-region environment with eventual consistency between regions.
Out of scope
- External threat intelligence integration: No direct integration with external threat feeds or third-party blocking services.
- Real-time content analysis: The system does not analyze content for blocking criteria, only checks against pre-defined block lists.
- User interface: No web UI or dashboard for managing blocks; only API access.
- Rate limiting per user/IP: While the system may block users, it does not implement rate limiting for individual users or IPs.
- Complex policy engine: No support for complex conditional blocking logic (e.g., "block if user has >5 failed logins in 1 hour and IP is from country X").
- Multi-tenancy: The system does not support multi-tenant environments or per-tenant block lists.
- Block propagation: Blocks created in one region do not automatically propagate to other regions (eventual consistency only).
2. API Design
API Style Selection
gRPC with Protocol Buffers is selected for the blocking system due to:
- Strong typing and automatic code generation
- Efficient binary serialization for high-throughput scenarios
- Native support for streaming and bidirectional communication
- Built-in service versioning capabilities
- Better performance characteristics than REST for internal microservices
Endpoint Definitions
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| CreateBlock | /v1/blocks | Creates a new block on a resource | JWT + RBAC | 100 req/min |
| GetBlock | /v1/blocks/{id} | Retrieves a specific block by ID | JWT + RBAC | 500 req/min |
| ListBlocks | /v1/blocks | Lists all active blocks with filters | JWT + RBAC | 200 req/min |
| ReleaseBlock | /v1/blocks/{id}/release | Releases a specific block | JWT + RBAC | 100 req/min |
| ExtendBlock | /v1/blocks/{id}/extend | Extends the duration of a block | JWT + RBAC | 50 req/min |
| ListBlockHistory | /v1/blocks/{id}/history | Gets historical changes for a block | JWT + RBAC | 100 req/min |
Request and Response Schemas
CreateBlockRequest
message CreateBlockRequest {
string resource_id = 1;
string resource_type = 2;
int64 duration_seconds = 3;
string reason = 4;
map<string, string> metadata = 5;
}
CreateBlockResponse
message CreateBlockResponse {
string block_id = 1;
string resource_id = 2;
string resource_type = 3;
int64 created_at = 4;
int64 expires_at = 5;
string reason = 6;
map<string, string> metadata = 7;
}
Block
message Block {
string block_id = 1;
string resource_id = 2;
string resource_type = 3;
int64 created_at = 4;
int64 expires_at = 5;
string reason = 6;
map<string, string> metadata = 7;
bool is_active = 8;
}
HTTP Status Codes and Error Format
Standard HTTP Statuses:
- 200 OK - Successful GET, PUT, PATCH, DELETE
- 201 Created - Successful POST
- 400 Bad Request - Invalid request body or parameters
- 401 Unauthorized - Missing or invalid authentication
- 403 Forbidden - Insufficient permissions
- 404 Not Found - Resource not found
- 429 Too Many Requests - Rate limit exceeded
- 500 Internal Server Error - Unexpected server error
Standardized Error Response:
{
"error": {
"code": "BLOCK_RESOURCE_CONFLICT",
"message": "Resource is already blocked",
"details": {
"block_id": "b1234567-890a-bcde-f012-34567890abcd",
"resource_id": "res-123",
"existing_block_expires_at": 1698765432
},
"timestamp": "2023-10-27T14:30:00Z"
}
}
API Versioning Strategy
Versioned using path prefix: /v1/...
- Major version changes require migration plan and deprecation timeline
- Minor versions can be backward compatible
- Versioning handled at service boundary, not within individual endpoints
Pagination Design
Cursor-based pagination is used because:
- More consistent and predictable than offset-based pagination
- Handles data changes during pagination gracefully
- Better performance for large datasets
Cursor format: cursor=eyJyZXNvdXJjZV9pZCI6InJlc3QtMTIzIiwic2V0X2F0IjoxNjk4NzY1NDMzfQ==
Authentication and Authorization
Authentication: JWT Bearer tokens with audience validation
Authorization: RBAC model with roles:
BLOCK_ADMIN: Full CRUD access to all blocksBLOCK_USER: Read access and self-managed blocks onlyBLOCK_SERVICE: Service-to-service access for internal systems
Rate Limiting
Per-endpoint rate limits:
CreateBlock: 100 req/min (token bucket algorithm)GetBlock: 500 req/min (fixed window)ListBlocks: 200 req/min (sliding window)ReleaseBlock: 100 req/min (token bucket)ExtendBlock: 50 req/min (token bucket)
Enforcement: In-memory token buckets with Redis persistence for cross-service consistency.
Idempotency Design
Idempotency Key Strategy:
- Client provides
X-Idempotency-Keyheader (UUID format) - Server stores operation state with key
- Subsequent identical requests return same result without side effects
- Key TTL: 5 minutes after last use
// Idempotency key validation in gRPC service
public CreateBlockResponse createBlock(CreateBlockRequest request, String idempotencyKey) {
if (idempotencyKey != null) {
String existingResult = idempotencyCache.get(idempotencyKey);
if (existingResult != null) {
return parseExistingResponse(existingResult);
}
}
// Process block creation...
String result = serializeResponse(response);
if (idempotencyKey != null) {
idempotencyCache.put(idempotencyKey, result, Duration.ofMinutes(5));
}
return response;
}
3. Data Model Design
Entity-Relationship Diagram
erDiagram
USER ||--o{ BLOCKING_REQUEST : "creates"
BLOCKING_REQUEST ||--o{ BLOCKING_RULE : "matches"
BLOCKING_RULE ||--o{ BLOCKING_REQUEST : "applies_to"
BLOCKING_REQUEST ||--o{ BLOCKING_HISTORY : "logs"
BLOCKING_HISTORY ||--o{ BLOCKING_EVENT : "records"
BLOCKING_RULE ||--o{ BLOCKING_EVENT : "triggers"
USER {
string id PK
string email
string name
timestamp created_at
timestamp updated_at
}
BLOCKING_REQUEST {
string id PK
string user_id FK
string resource_type
string resource_id
string reason
timestamp requested_at
timestamp resolved_at
string status
string resolution_note
timestamp created_at
timestamp updated_at
}
BLOCKING_RULE {
string id PK
string name
string description
string resource_type
string condition_json
timestamp created_at
timestamp updated_at
}
BLOCKING_HISTORY {
string id PK
string blocking_request_id FK
string status
timestamp timestamp
string note
timestamp created_at
}
BLOCKING_EVENT {
string id PK
string blocking_rule_id FK
string resource_type
string resource_id
timestamp triggered_at
string event_data_json
timestamp created_at
}
Table Schema Definitions
users
CREATE TABLE users (
id VARCHAR(36) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_created_at (created_at)
);
blocking_rules
CREATE TABLE blocking_rules (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
resource_type VARCHAR(100) NOT NULL,
condition_json JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_resource_type (resource_type),
INDEX idx_created_at (created_at)
);
blocking_requests
CREATE TABLE blocking_requests (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
resource_type VARCHAR(100) NOT NULL,
resource_id VARCHAR(255) NOT NULL,
reason TEXT NOT NULL,
requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
resolved_at TIMESTAMP NULL,
status ENUM('PENDING', 'APPROVED', 'REJECTED', 'EXPIRED') NOT NULL DEFAULT 'PENDING',
resolution_note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_resource (user_id, resource_type, resource_id),
INDEX idx_status (status),
INDEX idx_requested_at (requested_at),
INDEX idx_resolved_at (resolved_at),
INDEX idx_created_at (created_at)
);
blocking_history
CREATE TABLE blocking_history (
id VARCHAR(36) PRIMARY KEY,
blocking_request_id VARCHAR(36) NOT NULL,
status ENUM('PENDING', 'APPROVED', 'REJECTED', 'EXPIRED') NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (blocking_request_id) REFERENCES blocking_requests(id) ON DELETE CASCADE,
INDEX idx_blocking_request (blocking_request_id),
INDEX idx_timestamp (timestamp)
);
blocking_events
CREATE TABLE blocking_events (
id VARCHAR(36) PRIMARY KEY,
blocking_rule_id VARCHAR(36) NOT NULL,
resource_type VARCHAR(100) NOT NULL,
resource_id VARCHAR(255) NOT NULL,
triggered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
event_data_json JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (blocking_rule_id) REFERENCES blocking_rules(id) ON DELETE CASCADE,
INDEX idx_resource_type (resource_type),
INDEX idx_resource_id (resource_id),
INDEX idx_triggered_at (triggered_at),
INDEX idx_blocking_rule (blocking_rule_id)
);
Primary Key Strategy
UUIDs are used as primary keys for all tables to support:
- Distributed system compatibility (no coordination needed for ID generation)
- Audit trail integrity (no sequential patterns that could leak information)
- Horizontal sharding capability (UUIDs can be distributed across shards without collision)
Index Design
| Index Name | Fields | Type | Purpose |
|---|---|---|---|
idx_email | Standard | Fast user lookup by email | |
idx_resource_type | resource_type | Standard | Filter blocking rules by resource type |
idx_user_resource | user_id, resource_type, resource_id | Composite | Fast lookup of blocking requests for a specific user and resource |
idx_status | status | Standard | Fast filtering of blocking requests by status |
idx_requested_at | requested_at | Standard | Fast retrieval of recent blocking requests |
idx_resolved_at | resolved_at | Standard | Efficient query for expired or resolved requests |
idx_created_at | created_at | Standard | Audit trail and time-based queries |
idx_blocking_request | blocking_request_id | Standard | Fast history lookup for blocking requests |
idx_triggered_at | triggered_at | Standard | Efficient event timeline queries |
Data Partitioning / Sharding Strategy
- Shard by resource_type: Partition blocking_requests and blocking_events by resource type to reduce cross-shard queries
- Shard by user_id: For high-volume users, consider sharding blocking_requests by user_id hash to distribute load
- Time-based partitioning: For blocking_history, consider monthly partitions for large-scale historical queries
Soft Delete vs. Hard Delete Decision
Soft delete is used for blocking_requests and blocking_rules to maintain audit trail and allow for recovery in case of accidental deletion. The deleted_at timestamp field can be added to support soft deletes while maintaining referential integrity.
Audit Trail Design
All table modifications are logged with:
created_atandupdated_attimestamps- User who made the change (via user_id in blocking_requests)
- Audit records in blocking_history table for status changes
- Event logging in blocking_events for rule triggers
Data Archival Strategy and Retention Policy
-- Archive policy:
-- 1. blocking_requests older than 2 years → move to archive tables
-- 2. blocking_history older than 5 years → move to archive tables
-- 3. blocking_events older than 1 year → move to archive tables
CREATE TABLE blocking_requests_archive (
LIKE blocking_requests INCLUDING ALL
);
CREATE TABLE blocking_history_archive (
LIKE blocking_history INCLUDING ALL
);
CREATE TABLE blocking_events_archive (
LIKE blocking_events INCLUDING ALL
);
Retention policy:
- blocking_requests: 2 years (move to archive)
- blocking_history: 5 years (move to archive)
- blocking_events: 1 year (move to archive)
- blocking_rules: Indefinite (soft-deleted entries can be archived after 1 year)
4. Class Design & Service Layer
classDiagram
class BlockingSystem {
+createBlock(BlockRequest) BlockResponse
+getBlock(String) BlockResponse
+releaseBlock(String) ReleaseResponse
+extendBlock(String, Duration) ExtendResponse
+listActiveBlocks() List~Block~
}
class Block {
-id: String
-resourceId: String
-holderId: String
-createdAt: Instant
-expiresAt: Instant
-status: BlockStatus
-metadata: Map~String, String~
+isExpired() boolean
+canBeExtended() boolean
}
class BlockRequest {
resourceId: String
holderId: String
duration: Duration
metadata: Map~String, String~
}
class BlockResponse {
block: Block
success: boolean
message: String
}
class ReleaseResponse {
success: boolean
message: String
}
class ExtendResponse {
success: boolean
newExpiry: Instant
message: String
}
class BlockStatus {
PENDING
ACTIVE
EXPIRED
RELEASED
}
class BlockRepository {
+save(Block) Block
+findById(String) Optional~Block~
+findByResourceIdAndHolderId(String, String) Optional~Block~
+findActiveBlocksByResource(String) List~Block~
+update(Block) Block
+delete(Block) void
}
class EventPublisher {
+publish(BlockEvent) void
}
class BlockEvent {
eventType: String
blockId: String
resourceId: String
holderId: String
timestamp: Instant
}
class BlockValidator {
+validateCreateRequest(BlockRequest) ValidationResult
+validateReleaseRequest(String, String) ValidationResult
+validateExtendRequest(String, Duration) ValidationResult
}
class BlockService {
-repository: BlockRepository
-validator: BlockValidator
-publisher: EventPublisher
-maxExtensionDuration: Duration
-defaultBlockDuration: Duration
}
class BlockManager {
-blockService: BlockService
-lockManager: LockManager
}
class LockManager {
+acquireLock(String, String) boolean
+releaseLock(String, String) void
+isLocked(String, String) boolean
}
class BlockState {
-status: BlockStatus
-lock: ReentrantLock
-version: long
}
class BlockStateTransition {
+transitionToActive(Block) void
+transitionToExpired(Block) void
+transitionToReleased(Block) void
}
BlockingSystem --> BlockService : uses
BlockService --> BlockRepository : persists
BlockService --> BlockValidator : validates
BlockService --> EventPublisher : publishes
BlockService --> LockManager : manages concurrency
BlockService --> BlockStateTransition : handles state
Block --> BlockState : has state
BlockValidator --> BlockRequest : validates
BlockEvent --> Block : references
stateDiagram-v2
[*] --> PENDING
PENDING --> ACTIVE : validated and created
ACTIVE --> EXPIRED : timeout or manual release
ACTIVE --> RELEASED : explicit release
EXPIRED --> [*]
RELEASED --> [*]
Service Layer Overview
The blocking system is designed with a service-oriented architecture to handle concurrent access, state transitions, and distributed consistency. The core service is BlockService which orchestrates all blocking operations.
Service Class Structure and Responsibilities
- BlockService: Main orchestration layer handling business logic, validation, and state management
- BlockRepository: Data persistence abstraction for block entities
- BlockValidator: Input validation and business rule enforcement
- EventPublisher: Asynchronous event publishing for audit and monitoring
- LockManager: Concurrent access control for resource-level locking
- BlockStateTransition: State management and transition logic
Interface / Abstract Class Design
public interface BlockRepository {
Block save(Block block);
Optional<Block> findById(String id);
Optional<Block> findByResourceIdAndHolderId(String resourceId, String holderId);
List<Block> findActiveBlocksByResource(String resourceId);
Block update(Block block);
void delete(Block block);
}
public interface EventPublisher {
void publish(BlockEvent event);
}
public interface BlockValidator {
ValidationResult validateCreateRequest(BlockRequest request);
ValidationResult validateReleaseRequest(String blockId, String holderId);
ValidationResult validateExtendRequest(String blockId, Duration extension);
}
public enum BlockStatus {
PENDING,
ACTIVE,
EXPIRED,
RELEASED
}
Business Logic Encapsulation Patterns
The system uses the State Pattern to manage block lifecycle transitions:
public class BlockStateTransition {
private final Map<BlockStatus, Function<Block, Block>> stateTransitions;
public BlockStateTransition() {
stateTransitions = new HashMap<>();
stateTransitions.put(BlockStatus.PENDING, this::transitionToActive);
stateTransitions.put(BlockStatus.ACTIVE, this::transitionToExpired);
stateTransitions.put(BlockStatus.EXPIRED, this::transitionToReleased);
}
public void applyTransition(Block block) {
var transition = stateTransitions.get(block.getStatus());
if (transition != null) {
transition.apply(block);
}
}
private Block transitionToActive(Block block) {
block.setStatus(BlockStatus.ACTIVE);
return block;
}
private Block transitionToExpired(Block block) {
if (block.getExpiresAt().isBefore(Instant.now())) {
block.setStatus(BlockStatus.EXPIRED);
}
return block;
}
private Block blockToReleased(Block block) {
block.setStatus(BlockStatus.RELEASED);
return block;
}
}
Critical-Path Method Implementation
public BlockResponse createBlock(BlockRequest request) {
// Transaction boundary - all operations within single DB transaction
return transactionTemplate.execute(status -> {
try {
// Validate request
ValidationResult validation = validator.validateCreateRequest(request);
if (!validation.isValid()) {
throw new ValidationException("Invalid block request: " + validation.getErrorMessage());
}
// Acquire resource-level lock to prevent concurrent creation
String lockKey = "block:" + request.getResourceId();
boolean acquired = lockManager.acquireLock(lockKey, request.getHolderId());
if (!acquired) {
throw new ResourceLockedException("Resource " + request.getResourceId() +
" is currently blocked by another process");
}
// Check for existing active block
Optional<Block> existingBlock = repository.findByResourceIdAndHolderId(
request.getResourceId(), request.getHolderId());
if (existingBlock.isPresent() && !existingBlock.get().isExpired()) {
throw new BlockAlreadyExistsException("Resource " + request.getResourceId() +
" is already blocked by holder " + request.getHolderId());
}
// Create new block
Block block = Block.builder()
.id(UUID.randomUUID().toString())
.resourceId(request.getResourceId())
.holderId(request.getHolderId())
.createdAt(Instant.now())
.expiresAt(Instant.now().plus(request.getDuration()))
.status(BlockStatus.PENDING)
.metadata(request.getMetadata())
.build();
// Persist block
Block savedBlock = repository.save(block);
// Transition to active state
BlockStateTransition transition = new BlockStateTransition();
transition.applyTransition(savedBlock);
// Publish event
BlockEvent event = BlockEvent.builder()
.eventType("BLOCK_CREATED")
.blockId(savedBlock.getId())
.resourceId(savedBlock.getResourceId())
.holderId(savedBlock.getHolderId())
.timestamp(Instant.now())
.build();
publisher.publish(event);
// Release lock
lockManager.releaseLock(lockKey, request.getHolderId());
return BlockResponse.builder()
.block(savedBlock)
.success(true)
.message("Block created successfully")
.build();
} catch (Exception e) {
// Release lock on failure
try {
lockManager.releaseLock("block:" + request.getResourceId(), request.getHolderId());
} catch (Exception lockReleaseException) {
// Log but don't fail the transaction
logger.warn("Failed to release lock for resource: " + request.getResourceId(),
lockReleaseException);
}
throw e;
}
});
}
Transaction Boundary Design
All blocking operations are wrapped in database transactions to ensure atomicity:
- Creation, extension, and release operations must be atomic
- Lock acquisition is handled within the same transaction scope
- Events are published after successful persistence to maintain consistency
Service Interface Contracts
public interface BlockingSystem {
BlockResponse createBlock(BlockRequest request);
BlockResponse getBlock(String blockId);
ReleaseResponse releaseBlock(String blockId, String holderId);
ExtendResponse extendBlock(String blockId, Duration extension, String holderId);
List<Block> listActiveBlocks(String resourceId);
}
Saga / Orchestration Pattern
The blocking system does not require complex distributed transactions since all operations are within a single service boundary. However, for potential future expansion to distributed systems:
public class BlockSaga {
private final List<SagaStep> steps;
public void execute(SagaContext context) {
for (SagaStep step : steps) {
try {
step.execute(context);
} catch (Exception e) {
// Compensate previous steps
compensate(context, step);
throw new SagaExecutionException("Saga failed at step: " + step.getName(), e);
}
}
}
}
Domain Object / Value Object Design
@Value
public class Block {
String id;
String resourceId;
String holderId;
Instant createdAt;
Instant expiresAt;
BlockStatus status;
Map<String, String> metadata;
public boolean isExpired() {
return expiresAt.isBefore(Instant.now());
}
public boolean canBeExtended() {
return status == BlockStatus.ACTIVE && !isExpired();
}
}
@Value
public class BlockRequest {
String resourceId;
String holderId;
Duration duration;
Map<String, String> metadata;
}
@Value
public class BlockEvent {
String eventType;
String blockId;
String resourceId;
String holderId;
Instant timestamp;
}
5. Concurrency & Thread Safety
Concurrency Model
The system will use a thread pool-based concurrency model with Java's java.util.concurrent package. Each blocking operation will be handled by a dedicated thread from a fixed-size thread pool to prevent resource exhaustion. For high-concurrency scenarios, we'll leverage CompletableFuture and ExecutorService for asynchronous processing where appropriate.
Race Condition Prevention Strategy
Race conditions will be prevented through:
- Synchronized access to shared mutable state using
ReentrantLockorsynchronizedblocks - Atomic operations for counters and flags using
java.util.concurrent.atomicpackage - Immutable data structures where possible to eliminate the need for synchronization
- Thread-local storage for per-request state that doesn't need to be shared
Optimistic vs. Pessimistic Locking Selection
Optimistic locking will be used for blocking operations due to:
- High read-to-write ratio in most use cases
- Low probability of concurrent conflicts
- Better performance under moderate contention
- Reduced lock acquisition overhead
Implementation will use version numbers in database records to detect conflicts during updates.
Deadlock Prevention Strategy
Deadlock prevention will be implemented through:
- Lock ordering - All operations acquire locks in a consistent global order
- Timeout-based acquisition - Locks will timeout after 5 seconds to prevent indefinite blocking
- Deadlock detection - Periodic checks using thread dump analysis for debugging
- Hierarchical locking - Locks will be acquired in a strict hierarchy to prevent circular waits
Idempotency Implementation for Concurrent Duplicate Requests
Idempotency will be handled through:
- Idempotency keys - Unique identifiers provided by clients for duplicate request detection
- Request deduplication cache - In-memory LRU cache storing recent idempotency keys with TTL of 1 hour
- Database-level uniqueness constraints - Unique indexes on blocking request identifiers
public class BlockingService {
private final Map<String, BlockingRequest> idempotencyCache =
new ConcurrentHashMap<>();
private final ScheduledExecutorService cacheCleanupScheduler =
Executors.newScheduledThreadPool(1);
public BlockingResult createBlockingRequest(String idempotencyKey,
BlockingRequest request) {
// Check for duplicate request
if (idempotencyCache.containsKey(idempotencyKey)) {
return idempotencyCache.get(idempotencyKey).getResult();
}
// Process request with optimistic locking
BlockingResult result = processBlockingRequest(request);
// Cache the result for idempotency
idempotencyCache.put(idempotencyKey,
new BlockingRequest(request, result));
// Schedule cleanup of cache entry
cacheCleanupScheduler.schedule(
() -> idempotencyCache.remove(idempotencyKey),
1, TimeUnit.HOURS);
return result;
}
}
6. Sequence Diagrams for Key Flows
sequenceDiagram
participant U as User
participant API as API Gateway
participant Auth as Auth Service
participant BS as Blocking Service
participant DB as Database
participant Cache as Redis Cache
U->>API: Request to block resource
API->>Auth: Validate JWT token
Auth-->>API: Authenticated user
API->>BS: Call blockResource()
BS->>Cache: Check if resource is already blocked
Cache-->>BS: Not found
BS->>DB: Check resource availability
DB-->>BS: Resource available
BS->>DB: Insert blocking record
DB-->>BS: Success
BS->>Cache: Cache resource block
Cache-->>BS: Success
BS-->>API: Return success response
API-->>U: Block confirmation
sequenceDiagram
participant U as User
participant API as API Gateway
participant Auth as Auth Service
participant BS as Blocking Service
participant DB as Database
participant Cache as Redis Cache
U->>API: Request to block resource
API->>Auth: Validate JWT token
Auth-->>API: Authenticated user
API->>BS: Call blockResource()
BS->>Cache: Check if resource is already blocked
Cache-->>BS: Found existing block
BS->>DB: Check resource availability
DB-->>BS: Resource not available
BS-->>API: Return conflict error
API-->>U: Resource unavailable
sequenceDiagram
participant U as User
participant API as API Gateway
participant Auth as Auth Service
participant BS as Blocking Service
participant DB as Database
participant Cache as Redis Cache
participant MQ as Message Queue
U->>API: Request to block resource
API->>Auth: Validate JWT token
Auth-->>API: Authenticated user
API->>BS: Call blockResource()
BS->>Cache: Check if resource is already blocked
Cache-->>BS: Not found
BS->>DB: Check resource availability
DB-->>BS: Resource available
BS->>DB: Insert blocking record
DB-->>BS: Success
BS->>Cache: Cache resource block
Cache-->>BS: Success
BS->>MQ: Publish resourceBlocked event
MQ-->>BS: Event queued
BS-->>API: Return success response
API-->>U: Block confirmation
MQ->>Consumer: Consume resourceBlocked event
Consumer->>DB: Update related entities
Consumer-->>MQ: Acknowledge event
7. Caching Design
Cache Strategy Selection
For the blocking system, I'll use a cache-aside pattern with LRU eviction policy. This approach provides the best balance of performance and consistency for a blocking system where data freshness is important but not mission-critical.
The cache-aside pattern allows us to:
- Maintain control over when data is invalidated
- Handle cache misses gracefully with database fallback
- Support multiple cache tiers (application-level + distributed)
- Implement proper cache warming strategies
Cache Key Design
Cache keys will follow a structured namespace pattern:
{namespace}:{entity_type}:{entity_id}:{version}
Example keys for blocking system:
blocking:blocking_rule:12345:v1blocking:blocking_list:67890:v1blocking:user_block:alice:v1blocking:system_config:default:v1
Cache TTL Strategy
| Data Type | TTL | Reason |
|---|---|---|
| Blocking Rules | 5 minutes | Rules change infrequently, but need to be refreshed periodically |
| User Blocks | 1 hour | User blocking is relatively stable, but needs to be refreshed for consistency |
| System Configs | 10 minutes | Configuration changes are rare but should be propagated quickly |
| Active Blocks | 30 seconds | Real-time blocking status needs to be fresh |
Cache Eviction Policy
LRU (Least Recently Used) with TTL-based expiration:
- Primary eviction based on LRU to prevent memory bloat
- Secondary cleanup via TTL to ensure stale data doesn't persist indefinitely
- Cache warming for frequently accessed blocking rules
Cache Stampede Prevention
Implement probabilistic early expiry with distributed locks:
public class CacheStampedePrevention {
private static final double STAMPED_EXPIRY_FACTOR = 0.8;
public String getCachedValue(String key, Supplier<String> cacheMissHandler) {
String cachedValue = cache.get(key);
if (cachedValue != null && !isExpired(key)) {
return cachedValue;
}
// Implement distributed lock to prevent stampede
String lockKey = "lock:" + key;
if (distributedLock.tryAcquire(lockKey, 100)) {
try {
// Double-check pattern to avoid redundant computation
String freshValue = cache.get(key);
if (freshValue == null || isExpired(key)) {
freshValue = cacheMissHandler.get();
cache.put(key, freshValue, calculateExpiryTime());
}
return freshValue;
} finally {
distributedLock.release(lockKey);
}
} else {
// Fallback to stale cache or wait briefly
return cachedValue != null ? cachedValue : fallbackValue();
}
}
}
Distributed Cache Architecture
Using Redis Cluster with:
- 3 master nodes for high availability
- 3 replica nodes per master for read scaling
- Redis Sentinel for automatic failover
- Cache sharding based on entity type to prevent hot keys
Cache Invalidation Strategy
Event-driven invalidation with time-based TTLs:
@Component
public class BlockingCacheInvalidator {
@EventListener
public void handleBlockingRuleUpdated(BlockingRuleUpdatedEvent event) {
String cacheKey = "blocking:blocking_rule:" + event.getRuleId() + ":v1";
cache.invalidate(cacheKey);
// Also invalidate related user blocks that might be affected
String userBlockKey = "blocking:user_block:" + event.getAffectedUser() + ":v1";
cache.invalidate(userBlockKey);
}
@EventListener
public void handleUserBlocked(UserBlockedEvent event) {
String cacheKey = "blocking:user_block:" + event.getUserId() + ":v1";
cache.invalidate(cacheKey);
// Invalidate system-wide blocking status
String systemCacheKey = "blocking:system_config:default:v1";
cache.invalidate(systemCacheKey);
}
}
Cache Warm-up Strategy
Implement background warming for high-traffic blocking rules:
@Component
public class BlockingCacheWarmer {
@Scheduled(fixedRate = 300000) // Every 5 minutes
public void warmHighPriorityBlockingRules() {
List<BlockingRule> highPriorityRules = blockingRepository.findHighPriorityRules();
for (BlockingRule rule : highPriorityRules) {
String cacheKey = "blocking:blocking_rule:" + rule.getId() + ":v1";
String cachedValue = cache.get(cacheKey);
if (cachedValue == null) {
// Warm the cache with fresh data
String ruleData = blockingService.getBlockingRule(rule.getId());
cache.put(cacheKey, ruleData, calculateTTL(rule));
}
}
}
}
Cache Hit/Miss Ratio Monitoring
@Component
public class CacheMetrics {
private final MeterRegistry meterRegistry;
public void recordCacheHit(String cacheType) {
Counter.builder("cache.hits")
.tag("type", cacheType)
.register(meterRegistry)
.increment();
}
public void recordCacheMiss(String cacheType) {
Counter.builder("cache.misses")
.tag("type", cacheType)
.register(meterRegistry)
.increment();
}
}
This caching design provides optimal performance for a blocking system while maintaining data consistency through proper invalidation and stampede prevention mechanisms. The combination of cache-aside pattern, LRU eviction, and event-driven invalidation ensures that the system can handle high throughput while maintaining acceptable consistency levels.
8. Repository / Data Access Layer
Repository Pattern Implementation
The repository pattern is implemented to abstract data access logic and provide a clean separation between domain logic and database operations. Each entity will have its own repository interface and implementation.
// Repository Interface
public interface BlockingRepository {
Optional<Blocking> findById(UUID id);
List<Blocking> findByUserId(UUID userId);
List<Blocking> findByTargetIdAndType(String targetId, BlockingType type);
Blocking save(Blocking blocking);
void delete(UUID id);
boolean existsByUserIdAndTargetIdAndType(UUID userId, String targetId, BlockingType type);
}
// Repository Implementation
@Repository
public class BlockingRepositoryImpl implements BlockingRepository {
private final JdbcTemplate jdbcTemplate;
private final ObjectMapper objectMapper;
public BlockingRepositoryImpl(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
this.jdbcTemplate = jdbcTemplate;
this.objectMapper = objectMapper;
}
@Override
public Optional<Blocking> findById(UUID id) {
String sql = "SELECT * FROM blockings WHERE id = ?";
try {
Blocking blocking = jdbcTemplate.queryForObject(sql, new Object[]{id}, new BlockingRowMapper());
return Optional.of(blocking);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public List<Blocking> findByUserId(UUID userId) {
String sql = "SELECT * FROM blockings WHERE user_id = ? ORDER BY created_at DESC";
return jdbcTemplate.query(sql, new Object[]{userId}, new BlockingRowMapper());
}
@Override
public List<Blocking> findByTargetIdAndType(String targetId, BlockingType type) {
String sql = "SELECT * FROM blockings WHERE target_id = ? AND type = ? ORDER BY created_at DESC";
return jdbcTemplate.query(sql, new Object[]{targetId, type.name()}, new BlockingRowMapper());
}
@Override
public Blocking save(Blocking blocking) {
if (blocking.getId() == null) {
return create(blocking);
} else {
return update(blocking);
}
}
private Blocking create(Blocking blocking) {
String sql = """
INSERT INTO blockings (id, user_id, target_id, type, reason, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""";
UUID id = UUID.randomUUID();
blocking.setId(id);
blocking.setCreatedAt(Instant.now());
blocking.setUpdatedAt(Instant.now());
jdbcTemplate.update(sql,
id,
blocking.getUserId(),
blocking.getTargetId(),
blocking.getType().name(),
blocking.getReason(),
blocking.getCreatedAt(),
blocking.getUpdatedAt()
);
return blocking;
}
private Blocking update(Blocking blocking) {
String sql = """
UPDATE blockings
SET target_id = ?, type = ?, reason = ?, updated_at = ?
WHERE id = ?
""";
blocking.setUpdatedAt(Instant.now());
jdbcTemplate.update(sql,
blocking.getTargetId(),
blocking.getType().name(),
blocking.getReason(),
blocking.getUpdatedAt(),
blocking.getId()
);
return blocking;
}
@Override
public void delete(UUID id) {
String sql = "DELETE FROM blockings WHERE id = ?";
jdbcTemplate.update(sql, id);
}
@Override
public boolean existsByUserIdAndTargetIdAndType(UUID userId, String targetId, BlockingType type) {
String sql = "SELECT COUNT(*) FROM blockings WHERE user_id = ? AND target_id = ? AND type = ?";
Integer count = jdbcTemplate.queryForObject(sql, new Object[]{userId, targetId, type.name()}, Integer.class);
return count != null && count > 0;
}
}
ORM vs Raw Query Decisions
For the blocking system, raw SQL queries are preferred over ORM for the following reasons:
- Performance: Raw queries provide better performance for simple CRUD operations
- Control: Direct control over query execution and optimization
- Complexity: The blocking system has relatively simple data relationships that don't benefit from ORM complexity
However, for complex joins or aggregations, we would consider using JPA with proper indexing.
Read vs Write Separation
The system uses a read-write separation strategy:
// Read-Only Repository for high-volume reads
@Repository
public class BlockingReadRepository {
private final JdbcTemplate jdbcTemplate;
public BlockingReadRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// Optimized for read-heavy operations
public List<Blocking> findActiveBlockingsForTarget(String targetId, BlockingType type) {
String sql = """
SELECT * FROM blockings
WHERE target_id = ? AND type = ? AND created_at > ?
ORDER BY created_at DESC
""";
Instant cutoff = Instant.now().minus(Duration.ofHours(24));
return jdbcTemplate.query(sql, new Object[]{targetId, type.name(), cutoff}, new BlockingRowMapper());
}
}
// Write Repository for mutations
@Repository
public class BlockingWriteRepository {
private final JdbcTemplate jdbcTemplate;
public BlockingWriteRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Blocking createBlocking(Blocking blocking) {
String sql = """
INSERT INTO blockings (id, user_id, target_id, type, reason, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""";
blocking.setId(UUID.randomUUID());
blocking.setCreatedAt(Instant.now());
blocking.setUpdatedAt(Instant.now());
jdbcTemplate.update(sql,
blocking.getId(),
blocking.getUserId(),
blocking.getTargetId(),
blocking.getType().name(),
blocking.getReason(),
blocking.getCreatedAt(),
blocking.getUpdatedAt()
);
return blocking;
}
}
Connection Pool Configuration
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000
N+1 Query Prevention Strategy
The system prevents N+1 queries by:
- Eager Loading: Fetching all blocking records in a single query
- Batch Operations: Using batch inserts/updates where appropriate
// Batch operation for creating multiple blockings
public List<Blocking> createBlockings(List<Blocking> blockings) {
String sql = """
INSERT INTO blockings (id, user_id, target_id, type, reason, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""";
// Prepare batch
List<Object[]> batchArgs = blockings.stream()
.map(blocking -> new Object[]{
UUID.randomUUID(),
blocking.getUserId(),
blocking.getTargetId(),
blocking.getType().name(),
blocking.getReason(),
Instant.now(),
Instant.now()
})
.collect(Collectors.toList());
// Execute batch
jdbcTemplate.batchUpdate(sql, batchArgs);
return blockings;
}
Batch Operation Design
// Batch read operation for performance optimization
public Map<String, List<Blocking>> getBlockingsByTargetIds(List<String> targetIds) {
if (targetIds.isEmpty()) return Collections.emptyMap();
String placeholders = targetIds.stream()
.map(id -> "?")
.collect(Collectors.joining(",", "(", ")"));
String sql = """
SELECT * FROM blockings
WHERE target_id IN """ + placeholders + """
ORDER BY created_at DESC
""";
List<Blocking> allBlockings = jdbcTemplate.query(sql, targetIds.toArray(), new BlockingRowMapper());
return allBlockings.stream()
.collect(Collectors.groupingBy(Blocking::getTargetId));
}
Data Access Layer Design Principles
- Single Responsibility: Each repository handles one domain entity
- Consistent Interface: All repositories implement the same interface pattern
- Transaction Boundaries: Repository methods are designed to work within transaction contexts
- Performance Optimization: Read-heavy operations use optimized queries and caching strategies
- Type Safety: Strong typing with proper error handling for database operations
The repository layer is designed to be minimal and focused, ensuring that the domain logic in service classes can operate with clean data access patterns while maintaining high performance and scalability for the blocking system.
9. Asynchronous Processing & Messaging
Event-driven boundaries in this feature
The blocking system will use a hybrid synchronous/asynchronous approach. Critical operations like user blocking/unblocking and immediate policy enforcement will be handled synchronously to ensure atomicity and consistency. Non-critical operations such as audit logging, notifications, and analytics will be handled asynchronously to improve system responsiveness.
Key triggers for async processing:
- User blocking/unblocking operations (publish to blocking events topic)
- Policy violations detected by monitoring service (publish to policy violation topic)
- Periodic system health checks and reporting (scheduled async jobs)
Message schema design
{
"version": "1.0",
"event_type": "user_blocked",
"timestamp": "2023-10-15T14:30:00Z",
"user_id": "user-uuid-12345",
"blocked_by": "admin-uuid-67890",
"reason": "violation_of_terms",
"duration_minutes": 1440,
"metadata": {
"source": "policy_engine",
"ip_address": "192.168.1.100"
}
}
For versioning, we'll use semantic versioning with a version field in the message schema. Each event type will have its own versioned schema and consumers will be able to handle multiple versions through backward compatibility.
Dead letter queue strategy
All failed message processing will be routed to a dead-letter queue (DLQ) with a retention period of 7 days. Messages in DLQ will be tagged with failure reason and retry count.
Poison message handling:
- Messages failing more than 5 times will be automatically moved to DLQ
- DLQ messages will trigger alerts to on-call engineers
- Manual intervention required for DLQ message reprocessing
At-least-once vs. exactly-once delivery
We will implement at-least-once delivery with idempotency handling. This is chosen because:
- The blocking system operations are idempotent (blocking same user twice has same effect)
- Simpler to implement than exactly-once semantics
- Lower latency and better performance characteristics
Retry policy for consumers
Retry configuration:
- Maximum attempts: 5
- Backoff strategy: Exponential with jitter (1s, 2s, 4s, 8s, 16s)
- Retryable errors: Network timeouts, service unavailability, transient database errors
- Non-retryable errors: Invalid user ID, malformed message, permanent policy violations
Outbox pattern for reliable event publishing
We will implement the outbox pattern to ensure event delivery reliability:
@Component
public class BlockingEventPublisher {
private final BlockingEventRepository eventRepository;
private final KafkaTemplate<String, BlockingEvent> kafkaTemplate;
public void publishBlockingEvent(BlockingEvent event) {
// Store event in outbox table
BlockingEventOutbox outboxEvent = new BlockingEventOutbox(
event.getId(),
event.getEventType(),
event.toJson(),
"pending"
);
eventRepository.save(outboxEvent);
// Publish to Kafka
kafkaTemplate.send("blocking-events", event.getId(), event);
}
@Scheduled(fixedRate = 5000)
public void processPendingEvents() {
List<BlockingEventOutbox> pendingEvents = eventRepository.findPendingEvents();
for (BlockingEventOutbox outboxEvent : pendingEvents) {
try {
kafkaTemplate.send("blocking-events", outboxEvent.getEventId(),
parseEvent(outboxEvent.getPayload()));
eventRepository.markAsProcessed(outboxEvent.getId());
} catch (Exception e) {
// Log error and continue processing other events
eventRepository.markAsFailed(outboxEvent.getId(), e.getMessage());
}
}
}
}
Sequence diagram for async flows
sequenceDiagram
participant User as User Service
participant Blocking as Blocking Service
participant Kafka as Message Broker
participant Audit as Audit Service
participant Notification as Notification Service
User->>Blocking: Block User Request
Blocking->>Kafka: Publish UserBlocked Event
Kafka->>Audit: Consume Event (Async)
Kafka->>Notification: Consume Event (Async)
Audit-->>Kafka: Acknowledge
Notification-->>Kafka: Acknowledge
Kafka->>Blocking: Event Processed Successfully
10. Error Handling Design
Error Classification Hierarchy
Errors are classified into three main categories:
- Domain Errors - Business logic violations (e.g., insufficient funds, invalid state transitions)
- Infrastructure Errors - System-level issues (e.g., database connection failures, timeout exceptions)
- Validation Errors - Input validation failures (e.g., missing required fields, invalid format)
public enum ErrorType {
INSUFFICIENT_FUNDS,
INVALID_STATE_TRANSITION,
RESOURCE_NOT_FOUND,
DATABASE_CONNECTION_FAILURE,
TIMEOUT_ERROR,
VALIDATION_ERROR,
AUTHENTICATION_ERROR,
AUTHORIZATION_ERROR
}
Error Propagation Strategy
Errors are handled at the appropriate layer with minimal propagation:
- Service Layer: Handle domain-specific errors and convert to appropriate exceptions
- Controller Layer: Catch and transform to standardized HTTP responses
- Infrastructure Layer: Wrap external service errors in domain exceptions
Retry Strategy with Exponential Backoff and Jitter
For transient infrastructure errors, implement retry logic:
public class RetryableOperation {
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
private static final double JITTER_FACTOR = 0.2;
public <T> T executeWithRetry(Supplier<T> operation) {
int attempts = 0;
while (attempts < MAX_RETRIES) {
try {
return operation.get();
} catch (TransientException e) {
attempts++;
if (attempts >= MAX_RETRIES) {
throw new RetryExhaustedException("Operation failed after " + MAX_RETRIES + " attempts", e);
}
long delay = (long) (BASE_DELAY_MS * Math.pow(2, attempts) *
(1 + JITTER_FACTOR * (Math.random() - 0.5)));
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RetryExhaustedException("Retry interrupted", ie);
}
}
}
throw new RetryExhaustedException("Operation failed after "MAX_RETRIES + " attempts");
}
}
Circuit Breaker Design
Implement circuit breaker for external dependencies:
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 <T> T execute(Supplier<T> operation) {
switch (state) {
case CLOSED:
return executeClosed(operation);
case OPEN:
return executeOpen(operation);
case HALF_OPEN:
return executeHalfOpen(operation);
default:
throw new IllegalStateException("Unknown circuit state");
}
}
private <T> T executeClosed(Supplier<T> operation) {
try {
T result = operation.get();
onSuccess();
return result;
} catch (Exception e) {
onFailure(e);
throw e;
}
}
private <T> T executeOpen(Supplier<T> operation) {
if (System.currentTimeMillis() - lastFailureTime > timeoutMs) {
state = CircuitState.HALF_OPEN;
return executeHalfOpen(operation);
}
throw new CircuitBreakerOpenException("Circuit is OPEN");
}
private <T> T executeHalfOpen(Supplier<T> operation) {
try {
T result = operation.get();
state = CircuitState.CLOSED;
failureCount = 0;
return result;
} catch (Exception e) {
state = CircuitState.OPEN;
throw new CircuitBreakerOpenException("Circuit is OPEN after half-open attempt", e);
}
}
private void onSuccess() {
if (state == CircuitState.HALF_OPEN) {
failureCount = 0;
}
}
private void onFailure(Exception e) {
if (state == CircuitState.CLOSED) {
failureCount++;
lastFailureTime = System.currentTimeMillis();
if (failureCount >= failureThreshold) {
state = CircuitState.OPEN;
}
}
}
}
Fallback Behavior Design
Implement fallbacks for critical external dependencies:
public class FallbackService {
private final ExternalPaymentProvider paymentProvider;
public PaymentResult processPaymentWithFallback(PaymentRequest request) {
try {
return paymentProvider.processPayment(request);
} catch (ExternalServiceException e) {
// Fallback to internal payment processing
return fallbackToInternalProcessing(request);
}
}
private PaymentResult fallbackToInternalProcessing(PaymentRequest request) {
// Log the failure for monitoring
logger.warn("Falling back to internal payment processing due to external service failure",
request.getPaymentId());
// Implement fallback logic
return new PaymentResult("INTERNAL_FALLBACK", "Payment processed via fallback");
}
}
Error Logging Format and Alerting Thresholds
Structured logging with consistent format:
public class ErrorLogger {
private static final Logger logger = LoggerFactory.getLogger(ErrorLogger.class);
public void logError(String operation, String errorCode, String errorMessage,
Map<String, Object> context) {
Map<String, Object> logContext = new HashMap<>(context);
logContext.put("operation", operation);
logContext.put("error_code", errorCode);
logContext.put("message", errorMessage);
logContext.put("timestamp", Instant.now().toString());
logger.error("{}", JsonUtil.toJson(logContext));
}
public void alertOnCriticalError(String operation, String errorCode,
Exception exception) {
if (isAlertableError(errorCode)) {
// Send to monitoring system (e.g., PagerDuty, Slack)
sendAlert(operation, errorCode, exception);
}
}
private boolean isAlertableError(String errorCode) {
return Set.of("DATABASE_CONNECTION_FAILURE", "AUTHENTICATION_ERROR",
"PAYMENT_PROCESSING_FAILURE").contains(errorCode);
}
}
Alerting Thresholds
| Error Type | Warning Threshold | Critical Threshold | Alert Channel |
|---|---|---|---|
| DATABASE_CONNECTION_FAILURE | 5/min | 10/min | Slack |
| AUTHENTICATION_ERROR | 3/min | 5/min | PagerDuty |
| PAYMENT_PROCESSING_FAILURE | 2/min | 5/min | PagerDuty |
| TIMEOUT_ERROR | 10/min | 20/min | Slack |
Error Response Format
Standardized JSON error response:
{
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "Insufficient funds for payment",
"details": {
"required_amount": 100.00,
"available_balance": 50.00
},
"timestamp": "2023-10-15T14:30:00Z",
"request_id": "req-12345"
}
}
Error Handling in Service Layer
public class BlockingService {
private final ErrorLogger errorLogger;
public BlockingResult blockResource(String resourceId, BlockRequest request) {
try {
// Business logic validation
validateBlockRequest(request);
// Check existing blocks
if (hasActiveBlock(resourceId)) {
throw new ResourceAlreadyBlockedError("Resource is already blocked");
}
// Perform blocking operation
BlockingRecord block = createBlock(resourceId, request);
// Publish event for async processing
eventPublisher.publish(new ResourceBlockedEvent(resourceId, block));
return new BlockingResult(block);
} catch (ValidationException e) {
errorLogger.logError("BLOCK_RESOURCE", "VALIDATION_ERROR",
e.getMessage(), Map.of("resource_id", resourceId));
throw new BadRequestException("Invalid block request", e);
} catch (ResourceAlreadyBlockedError e) {
errorLogger.logError("BLOCK_RESOURCE", "RESOURCE_ALREADY_BLOCKED",
e.getMessage(), Map.of("resource_id", resourceId));
throw new ConflictException("Resource already blocked", e);
} catch (DatabaseException e) {
errorLogger.logError("BLOCK_RESOURCE", "DATABASE_ERROR",
"Database operation failed", Map.of("resource_id", resourceId));
throw new ServiceUnavailableException("Service temporarily unavailable", e);
} catch (Exception e) {
errorLogger.logError("BLOCK_RESOURCE", "UNEXPECTED_ERROR",
"Unexpected error occurred", Map.of("resource_id", resourceId));
throw new InternalServerErrorException("Internal server error", e);
}
}
}
11. Security Design
Input Validation and Sanitization Strategy
All inputs undergo strict validation using a combination of:
- Regex patterns for format validation (e.g., email, phone number)
- Length constraints to prevent buffer overflow attacks
- Type checking to ensure expected data types
- Custom validators for business rule compliance
public class InputValidator {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$");
public static boolean isValidEmail(String email) {
return email != null && EMAIL_PATTERN.matcher(email).matches();
}
public static boolean isValidStringLength(String input, int min, int max) {
return input != null && input.length() >= min && input.length() <= max;
}
}
SQL Injection Prevention
All database queries use parameterized prepared statements exclusively:
- Never concatenate user input into SQL strings
- Use ORM with parameter binding (e.g., JPA/Hibernate)
- Whitelist allowed characters for specific fields
Authentication Middleware Design
Authentication middleware intercepts all requests and:
- Verifies JWT tokens for authenticated endpoints
- Extracts user context from token claims
- Sets security context in thread-local storage
@Component
public class AuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String token = extractToken(httpRequest);
if (token != null && validateToken(token)) {
SecurityContext context = createSecurityContext(token);
SecurityContextHolder.setContext(context);
}
chain.doFilter(request, response);
}
}
Authorization Enforcement
RBAC-based authorization with role hierarchy:
- User roles: ADMIN, MODERATOR, USER
- Resource permissions: READ, WRITE, DELETE
- Permission checking at service layer level
@PreAuthorize("hasRole('ADMIN') or hasPermission(#userId, 'USER', 'READ')")
public User getUserById(String userId) {
return userRepository.findById(userId);
}
Sensitive Data Encryption
At Rest: AES-256 encryption with key rotation every 90 days using AWS KMS In Transit: TLS 1.3 with HSTS header enforcement
@Service
public class DataEncryptionService {
private final AesGcmEncryptor encryptor = new AesGcmEncryptor();
public String encrypt(String plaintext) {
return encryptor.encrypt(plaintext, getEncryptionKey());
}
public String decrypt(String ciphertext) {
return encryptor.decrypt(ciphertext, getEncryptionKey());
}
}
PII Handling and Masking Strategy
PII fields (SSN, email, phone) are:
- Masked in logs using format:
***-**-**** - Redacted from API responses when not required
- Stored encrypted in database
Secrets Management
Secrets are managed through AWS Secrets Manager:
- Database credentials rotated every 30 days
- API keys stored in encrypted format
- Environment-specific secret paths with IAM role-based access
Audit Logging Design
All security-relevant events are logged with:
- Required fields: timestamp, user_id, action_type, ip_address, session_id
- PII masking in logs using redaction rules
- Log retention: 90 days with archiving to S3
@EventListener
public void handleSecurityEvent(SecurityEvent event) {
log.info("Security Event - User: {}, Action: {}, IP: {}",
event.getUserId(), event.getAction(), event.getIpAddress());
}
12. Testing Strategy
Unit Testing Scope and Coverage Targets (%)
- Service Layer: 85% code coverage, focusing on business logic and edge cases
- Repository Layer: 90% coverage with all CRUD operations tested
- Domain Objects: 100% coverage for value objects and immutable entities
- Exception Handling: 100% coverage for all expected error paths
- Integration Points: 95% coverage for external service mocks and database interactions
Integration Testing Strategy
- Database Integration: Use Testcontainers with PostgreSQL to test actual database interactions with real schema and constraints
- External Service Mocks: WireMock for third-party API integrations (payment gateway, notification service)
- Event System: Embedded Kafka or Redis streams for testing event-driven flows
- Caching Layer: Use embedded Redis instances for cache interaction testing
Contract Testing
- API Contracts: Implement Pact-based contract tests between service and consumer teams
- Event Schemas: Use Avro schema registry with schema validation tests for message format consistency
- External API Contracts: Mock external service contracts to ensure compatibility and prevent breaking changes
Performance Testing Tool and Key Scenarios
- Load Testing: Use k6 for load testing with realistic user scenarios (e.g., concurrent blocking requests, bulk operations)
- Stress Testing: Simulate peak load conditions to identify bottlenecks in blocking logic and database queries
- Key Scenarios:
- Concurrent blocking operations (500+ simultaneous requests)
- Bulk blocking/unblocking operations
- High-frequency blocking status queries
- Database connection pool exhaustion under load
Test Data Management Strategy
- Fixtures: Use factory patterns to generate realistic test data for blocking scenarios (e.g., user, resource, and time-based blocking)
- Test Data Generators: Implement data generation libraries to create varied test cases (e.g., overlapping blocking periods, edge case time boundaries)
- Seeding Strategy: Use database migration scripts with test data for consistent baseline state across environments
- Test Data Isolation: Use database transactional isolation and schema-per-test for parallel test execution
// Example unit test for blocking service with mock dependencies
@Test
public void testBlockResource_Success() {
// Given
Resource resource = Resource.builder().id("resource-1").build();
BlockingRequest request = BlockingRequest.builder()
.userId("user-1")
.resourceId("resource-1")
.startTime(Instant.now())
.endTime(Instant.now().plus(Duration.ofHours(1)))
.build();
when(resourceRepository.findById("resource-1")).thenReturn(Optional.of(resource));
when(blockingRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
// When
BlockingResult result = blockingService.blockResource(request);
// Then
assertThat(result.isSuccess()).isTrue();
verify(blockingRepository).save(any());
}
13. Observability Design
Structured Logging Schema
All logs follow a structured JSON format with consistent fields to enable programmatic parsing and filtering:
{
"timestamp": "2023-10-27T14:30:45.123Z",
"level": "INFO",
"service": "blocking-service",
"trace_id": "12345678-1234-1234-1234-123456789012",
"span_id": "abcdef12-3456-7890-abcd-ef1234567890",
"user_id": "user_12345",
"entity_id": "block_98765",
"operation": "createBlock",
"request_id": "req_abc123",
"message": "Block created successfully",
"duration_ms": 45,
"error_code": null,
"error_message": null,
"stack_trace": null,
"metadata": {
"block_type": "user",
"blocked_user_id": "blocked_54321"
}
}
PII masking rules:
- User IDs are logged with obfuscation (e.g.,
user_12345→user_XXXXX) - Email addresses are masked (e.g.,
john.doe@example.com→j***.d***@example.com) - IP addresses are truncated to network level (e.g.,
192.168.1.100→192.168.1.0)
Distributed Tracing Instrumentation
OpenTelemetry is used for distributed tracing with custom spans:
Key Operations to Trace:
blocking.createBlock- Span for creating a new blockblocking.checkBlockingStatus- Span for checking if user is blockedblocking.removeBlock- Span for removing a block
Span Attributes Captured:
span.setAttribute("block.type", "user");
span.setAttribute("block.target_id", block.getTargetId());
span.setAttribute("block.created_by", block.getCreatedBy());
span.setAttribute("request.user_id", userId);
span.setAttribute("request.block_type", blockType);
Metrics Design
RED Method Metrics:
| Metric Name | Type | Labels | Description |
|---|---|---|---|
blocking.requests.total | Counter | operation, status | Total count of blocking operations |
blocking.requests.duration | Histogram | operation, status | Latency distribution of blocking operations |
blocking.errors.total | Counter | operation, error_type | Count of errors by operation and type |
blocking.active_blocks | Gauge | block_type, status | Current count of active blocks |
Example Histogram Bucket Configuration:
// For blocking.requests.duration histogram
Histogram.Bucket[] buckets = {
5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 2500.0, 5000.0
};
Health Check Endpoint Design
@RestController
@RequestMapping("/health")
public class HealthController {
@GetMapping("/live")
public ResponseEntity<Map<String, Object>> live() {
Map<String, Object> response = new HashMap<>();
response.put("status", "healthy");
response.put("timestamp", Instant.now());
return ResponseEntity.ok(response);
}
@GetMapping("/ready")
public ResponseEntity<Map<String, Object>> ready() {
Map<String, Object> response = new HashMap<>();
response.put("status", "ready");
response.put("timestamp", Instant.now());
response.put("database", databaseHealthCheck());
response.put("cache", cacheHealthCheck());
response.put("message_queue", messageQueueHealthCheck());
return ResponseEntity.ok(response);
}
@GetMapping("/startup")
public ResponseEntity<Map<String, Object>> startup() {
Map<String, Object> response = new HashMap<>();
response.put("status", "startup_complete");
response.put("timestamp", Instant.now());
response.put("dependencies", getDependencyStatuses());
return ResponseEntity.ok(response);
}
}
Alerting Thresholds
Critical Alerts (Escalation Path: On-call engineer → Engineering Manager):
blocking.requests.duration.p99> 500ms for 5 minutesblocking.errors.total> 100 in 5 minutes (per operation)blocking.active_blocks> 1M (sudden spike)
Warning Alerts (Escalation Path: On-call engineer):
blocking.requests.duration.p99> 200ms for 15 minutesblocking.errors.total> 50 in 15 minutes (per operation)- Database connection pool utilization > 90% for 10 minutes
Service Level Indicators (SLIs):
- 99.9% of blocking operations complete within 200ms (p99 latency)
- 99.9% of blocking operations succeed (error rate < 0.1%)
Error Budget:
- 0.1% error budget (99.9% availability)
- Alert when error budget consumption exceeds 50% of allocated budget
14. Open Questions, Risks & Trade-offs
Decisions requiring product / engineering alignment
- Blocking scope definition: Should blocking apply to all user activities or only specific actions (e.g., booking, payment)? This impacts the system's attack surface and complexity.
- User experience vs. security trade-off: How granular should blocking be? Fine-grained blocking (per action) vs. coarse-grained (per user/session) affects both security and UX.
- Blocking duration strategy: Should blocking durations be fixed or dynamically calculated based on risk scoring? This affects the system's adaptability to different threat patterns.
Known trade-offs and justifications
| Trade-off | Option A | Option B | Justification |
|---|---|---|---|
| Blocking granularity | Per-user/session | Per-action/endpoint | Per-user provides better performance but less security; per-action offers granular control at cost of increased complexity and potential false positives |
| Blocking enforcement timing | Real-time blocking | Batch processing | Real-time provides better UX but increases latency; batch processing reduces system load but delays blocking |
| Blocking state persistence | In-memory only | Database-backed | In-memory offers better performance but loses state on restart; database persistence ensures durability but adds latency |
| Blocking policy enforcement | Centralized policy engine | Distributed rule evaluation | Centralized provides consistency but creates single point of failure; distributed allows scaling but increases complexity |
Risks and mitigation strategies
- Risk: False positives leading to legitimate user blocking
- Mitigation: Implement a review queue with manual override capability and user appeal process
- Risk: Blocking system becoming a performance bottleneck
- Mitigation: Use asynchronous processing for non-critical blocking decisions, implement circuit breaker patterns, and cache frequently accessed blocking rules
- Risk: Blocking bypass through API gateway or direct database access
- Mitigation: Implement blocking at multiple layers (API gateway, service layer, database level) and enforce strict access control
- Risk: Blocking rules becoming outdated or ineffective
- Mitigation: Implement automated rule lifecycle management with periodic review and A/B testing capabilities
Technical debt acknowledged
- Debt: Hardcoded blocking thresholds in service layer instead of configurable policy engine
- Impact: Requires code changes for threshold adjustments, increases deployment frequency
- Mitigation: Plan migration to externalized policy configuration system in next sprint
- Debt: Single-threaded blocking logic that may not scale to high-concurrency scenarios
- Impact: Performance degradation under heavy load, potential blocking queue bottlenecks
- Mitigation: Design for horizontal scaling with sharding strategy and consider async processing patterns
- Debt: Limited audit trail for blocking decisions (only user ID and timestamp)
- Impact: Difficult to analyze blocking patterns, debug false positives, or comply with audit requirements
- Mitigation: Extend audit logging to capture decision context, risk scores, and triggering events