Skip to main content

Ranking Engine Design: Scalable Content Ranking System

Comprehensive low-level design for a scalable ranking engine that processes millions of content items with real-time scoring and sorting capabilities.

A ranking engine is a critical backend system that determines the order of content items based on various factors like relevance, popularity, and user behavior. This document presents a scalable architecture for processing millions of content items with real-time scoring capabilities and efficient sorting mechanisms.

1. Problem Statement & Requirements

Clarifying questions

  1. What is the scope of ranking? Is this for content (e.g., articles, videos), products, users, or a combination? Are we building a general-purpose ranking engine or one tailored to specific use cases?
  2. What is the expected scale? What's the anticipated volume of items to rank, and how many concurrent ranking requests per second? Will we need to support real-time ranking or batch processing?
  3. What consistency requirements are there? Should rankings be strongly consistent, eventually consistent, or can they tolerate some staleness for performance?
  4. What's the read/write ratio? Is this primarily a read-heavy system where rankings are frequently accessed, or do we expect frequent updates to items and re-ranking?
  5. Latency sensitivity? What are the latency SLAs for ranking operations (e.g., p50/p95/p99)? Are there different SLAs for real-time vs. batch ranking?
  6. What authentication model is expected? Will this engine be used internally or exposed to external consumers? What auth/permission model is required?
  7. Are there any external integrations or dependencies? Will the ranking engine integrate with other systems like search, recommendation engines, or analytics platforms?

Functional requirements

  • Rank items based on configurable criteria - Support custom ranking rules and weights for different metrics (e.g., popularity, recency, relevance)
  • Support real-time and batch ranking - Handle both immediate ranking requests and scheduled batch recomputations
  • Provide ranking context - Return detailed ranking information (score breakdown, rank position, confidence)
  • Support multiple ranking modes - Allow configuration of different ranking strategies (e.g., popularity-based, recency-based, custom algorithm)
  • Support filtering and faceting - Apply rankings within filtered or faceted contexts (e.g., "top 10 articles in tech category")
  • Support ranking persistence - Store and retrieve rankings with versioning capability
  • Support ranking history tracking - Maintain historical ranking data for analysis and rollback capability

Non-functional requirements

  • Latency targets:
    • p50: <50ms for real-time ranking requests
    • p95: <200ms for real-time ranking requests
    • p99: <500ms for real-time ranking requests
  • Throughput:
    • 10,000+ real-time ranking requests per second
    • 100,000+ batch ranking operations per hour
  • Availability:
    • 99.9% uptime (28.8 minutes of downtime/month max)
  • Scalability:
    • Support horizontal scaling across multiple nodes
    • Handle 10M+ items in the ranking system

Constraints and assumptions

  • Assumption: The engine will primarily serve read-heavy workloads with infrequent updates to items and their ranking criteria
  • Constraint: Ranking algorithms must be deterministic to ensure consistency across sessions
  • Assumption: Items will have a stable identifier (e.g., UUID) that persists across ranking operations
  • Constraint: Ranking engine will not handle item creation or deletion - these are handled by other systems
  • Assumption: Ranking weights and configurations can be updated without downtime

Out of scope

  • Item creation/deletion: The engine does not handle item lifecycle management
  • Search integration: Full-text search capabilities are out of scope (will integrate with existing search systems)
  • User personalization: Individual user preferences and personalization are out of scope (will integrate with recommendation systems)
  • Real-time analytics: Real-time metrics and dashboards for ranking performance are out of scope
  • Machine learning model training: The engine does not train or manage ML models for ranking, only applies pre-trained models
  • Cross-system data synchronization: Handling data consistency across multiple systems is out of scope (will rely on event-driven architecture)

2. API Design

API Style Selection

gRPC with Protocol Buffers is selected for the ranking engine due to:

  • Strong typing and schema evolution support
  • Efficient binary serialization for high-throughput ranking operations
  • Built-in support for streaming and bidirectional communication
  • Native support for versioning through proto file management
  • Better performance characteristics for real-time ranking scenarios

API Endpoints

MethodPathDescriptionAuth
POST/v1/rankRank items based on provided criteria and weightsJWT
GET/v1/rank/{id}Retrieve ranked items for a specific ranking sessionJWT
PUT/v1/rank/{id}Update existing ranking criteria and weightsJWT
DELETE/v1/rank/{id}Delete a ranking sessionJWT
POST/v1/rank/previewPreview ranking results without persistingJWT
GET/v1/rank/historyRetrieve historical ranking sessionsJWT

Request and Response Schemas

RankRequest (Protocol Buffer):

message RankRequest {
  string ranking_id = 1;
  repeated Item items = 2;
  RankingCriteria criteria = 3;
  string user_id = 4;
  int32 page_size = 5;
  string cursor = 6;
}

message Item {
  string id = 1;
  map<string, double> features = 2;
  string category = 3;
}

message RankingCriteria {
  map<string, double> weights = 1;
  string sort_by = 2; // "relevance", "popularity", "timestamp"
  repeated string excluded_categories = 3;
}

RankResponse (Protocol Buffer):

message RankResponse {
  string ranking_id = 1;
  repeated RankedItem ranked_items = 2;
  int32 total_count = 3;
  string next_cursor = 4;
  double score_threshold = 5;
}

message RankedItem {
  string item_id = 1;
  double score = 2;
  int32 rank = 3;
  map<string, double> feature_scores = 4;
}

HTTP Status Codes and Error Response

Standardized Error Format:

{
  "error": {
    "code": "INVALID_ARGUMENT",
    "message": "Missing required field: criteria",
    "details": [
      {
        "field": "criteria",
        "reason": "Required field is missing"
      }
    ]
  }
}

Status Codes:

  • 200 OK - Successful ranking
  • 201 Created - New ranking created
  • 400 Bad Request - Invalid input parameters
  • 401 Unauthorized - Missing or invalid auth token
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Ranking session not found
  • 429 Too Many Requests - Rate limit exceeded
  • 500 Internal Server Error - Server-side failure

API Versioning Strategy

Protocol Buffer Versioning:

  • Major version (v1) for breaking changes
  • Minor versions within same major version for backward-compatible additions
  • Field numbers preserved across versions to maintain compatibility

Pagination Design

Cursor-based Pagination is implemented because:

  • More stable and consistent across data changes
  • Better performance for large datasets
  • Handles concurrent modifications gracefully

Cursor Format:

{
  "cursor": "eyJpZCI6IjEyMzQ1Iiwic2NvcmUiOjAuOTUsInJhbmsiOjF9"
}

Authentication and Authorization

Authentication:

  • JWT tokens with HS256 signing
  • Token expiration set to 24 hours
  • Refresh token support for long-lived sessions

Authorization:

  • RBAC model with roles: admin, user, service
  • User-specific ranking sessions with ownership checks
  • Service accounts for system-to-system communication

Rate Limiting

Rate Limits:

EndpointLimitPeriod
/v1/rank1000 requests1 minute
/v1/rank/preview5000 requests1 minute
/v1/rank/history200 requests1 minute

Enforcement Strategy:

  • In-memory token bucket per user
  • Redis-based distributed rate limiting for multi-instance deployments
  • Burst handling with configurable max burst size

Idempotency Design

Idempotency Key Strategy:

  • Client-provided idempotency_key header (UUID format)
  • Server-side cache with TTL of 5 minutes
  • Same operation with same key returns cached result
  • Used for POST /v1/rank and PUT /v1/rank/{id} operations

Example Request/Response

Request:

{
  "ranking_id": "rank-12345",
  "items": [
    {
      "id": "item-1",
      "features": {
        "popularity": 0.8,
        "relevance": 0.9
      },
      "category": "electronics"
    }
  ],
  "criteria": {
    "weights": {
      "popularity": 0.6,
      "relevance": 0.4
    },
    "sort_by": "relevance"
  }
}

Response:

{
  "ranking_id": "rank-12345",
  "ranked_items": [
    {
      "item_id": "item-1",
      "score": 0.86,
      "rank": 1,
      "feature_scores": {
        "popularity": 0.48,
        "relevance": 0.34
      }
    }
  ],
  "total_count": 1,
  "next_cursor": null
}

3. Data Model Design

Entity-Relationship Diagram

erDiagram
    USER {
        string id PK
        string username
        string email
        timestamp created_at
        timestamp updated_at
    }

    CONTENT {
        string id PK
        string title
        string description
        string content_type
        timestamp created_at
        timestamp updated_at
        boolean is_active
    }

    RANKING {
        string id PK
        string content_id FK
        string user_id FK
        integer score
        timestamp created_at
        timestamp updated_at
    }

    RANKING_HISTORY {
        string id PK
        string ranking_id FK
        integer old_score
        integer new_score
        timestamp changed_at
        string changed_by
    }

    USER_CONTENT_SCORE {
        string user_id FK
        string content_id FK
        integer score
        timestamp last_updated
        primary key (user_id, content_id)
    }

    USER_CONTENT_SCORE_HISTORY {
        string user_id FK
        string content_id FK
        integer old_score
        integer new_score
        timestamp changed_at
        string changed_by
        primary key (user_id, content_id, changed_at)
    }

    USER --> RANKING : creates
    CONTENT --> RANKING : contains
    RANKING --> RANKING_HISTORY : logs changes
    USER_CONTENT_SCORE --> USER_CONTENT_SCORE_HISTORY : logs changes

Table Schema Definitions

users

CREATE TABLE users (
    id VARCHAR(36) PRIMARY KEY,
    username VARCHAR(255) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);

content

CREATE TABLE content (
    id VARCHAR(36) PRIMARY KEY,
    title VARCHAR(500) NOT NULL,
    description TEXT,
    content_type VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    is_active BOOLEAN DEFAULT TRUE
);

CREATE INDEX idx_content_type ON content(content_type);
CREATE INDEX idx_content_active ON content(is_active);

rankings

CREATE TABLE rankings (
    id VARCHAR(36) PRIMARY KEY,
    content_id VARCHAR(36) NOT NULL,
    user_id VARCHAR(36) NOT NULL,
    score INTEGER NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    
    UNIQUE KEY uk_user_content (user_id, content_id),
    INDEX idx_content_score (content_id, score DESC),
    INDEX idx_user_score (user_id, score DESC)
);

ranking_history

CREATE TABLE ranking_history (
    id VARCHAR(36) PRIMARY KEY,
    ranking_id VARCHAR(36) NOT NULL,
    old_score INTEGER NOT NULL,
    new_score INTEGER NOT NULL,
    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    changed_by VARCHAR(36),
    
    FOREIGN KEY (ranking_id) REFERENCES rankings(id) ON DELETE CASCADE
);

CREATE INDEX idx_ranking_history_ranking ON ranking_history(ranking_id);
CREATE INDEX idx_ranking_history_changed_at ON ranking_history(changed_at);

user_content_scores

CREATE TABLE user_content_scores (
    user_id VARCHAR(36) NOT NULL,
    content_id VARCHAR(36) NOT NULL,
    score INTEGER NOT NULL DEFAULT 0,
    last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    PRIMARY KEY (user_id, content_id),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE
);

CREATE INDEX idx_user_content_scores_user ON user_content_scores(user_id);
CREATE INDEX idx_user_content_scores_content ON user_content_scores(content_id);

user_content_scores_history

CREATE TABLE user_content_scores_history (
    user_id VARCHAR(36) NOT NULL,
    content_id VARCHAR(36) NOT NULL,
    old_score INTEGER NOT NULL,
    new_score INTEGER NOT NULL,
    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    changed_by VARCHAR(36),
    
    PRIMARY KEY (user_id, content_id, changed_at),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE
);

CREATE INDEX idx_user_content_scores_history_user ON user_content_scores_history(user_id);
CREATE INDEX idx_user_content_scores_history_content ON user_content_scores_history(content_id);

Primary Key Strategy

UUIDs are used as primary keys for the following reasons:

  1. Distributed system compatibility: UUIDs can be generated without coordination across services
  2. Security: Prevents enumeration attacks since IDs are not sequential
  3. Scalability: No contention on ID generation in high-concurrency environments
  4. Global uniqueness: Ensures no collisions across distributed systems

Index Design Justification

  1. rankings.idx_content_score:

    • Covers queries for content rankings by score
    • Composite index on (content_id, score DESC) enables efficient top-N queries
  2. rankings.idx_user_score:

    • Supports user-specific ranking lookups
    • Enables efficient queries for a user's content scores
  3. users.idx_users_email:

    • Supports email-based lookups for authentication and user management
  4. content.idx_content_type:

    • Enables filtering by content type for different ranking algorithms
  5. user_content_scores.idx_user_content_scores_user:

    • Optimizes user score lookups for recommendation systems
  6. ranking_history.idx_ranking_history_ranking:

    • Supports audit trail queries for specific rankings

Data Partitioning Strategy

Partition content by content_type using hash partitioning:

PARTITION BY HASH(content_type) PARTITIONS 4;

Partition rankings by content_id hash:

PARTITION BY HASH(content_id) PARTITIONS 8;

This ensures even distribution of content and allows for horizontal scaling.

Soft Delete vs. Hard Delete

Soft delete is implemented using is_active boolean flag on content table:

  • Content can be marked inactive rather than deleted
  • Allows for recovery and audit trail preservation
  • Maintains referential integrity in rankings table

Audit Trail Design

All modifications to ranking scores are logged in ranking_history and user_content_scores_history tables:

  • Records old vs new score values
  • Captures timestamp of change
  • Tracks who made the change (user_id)
  • Supports rollback and audit capabilities

Data Archival Strategy

Content older than 2 years will be archived to cold storage:

  • Move inactive content to content_archive table
  • Keep rankings in active tables for performance
  • Archive user scores with content to maintain relationships
  • Retention policy: 3 years for active content, indefinite for historical rankings
-- Archival job would move data like:
INSERT INTO content_archive SELECT * FROM content WHERE created_at < DATE_SUB(NOW(), INTERVAL 2 YEAR);
DELETE FROM content WHERE created_at < DATE_SUB(NOW(), INTERVAL 2 YEAR);

4. Class Design & Service Layer

UML Class Diagram

classDiagram
    class RankingEngine {
        +rankItems(items: List~Item~, criteria: List~RankingCriteria~) List~RankedItem~
        +updateRanking(item: Item, score: Double) void
    }

    class Item {
        +id: String
        +name: String
        +score: Double
        +lastUpdated: Instant
        +metadata: Map~String, Object~
    }

    class RankingCriteria {
        +id: String
        +name: String
        +weight: Double
        +type: CriteriaType
        +config: Map~String, Object~
    }

    class RankedItem {
        +item: Item
        +rank: Integer
        +score: Double
    }

    class ScoreCalculator {
        +calculateScore(item: Item, criteria: List~RankingCriteria~) Double
    }

    class WeightedScoreCalculator {
        +calculateWeightedScore(item: Item, criteria: List~RankingCriteria~) Double
    }

    class RankingStrategy {
        +rank(items: List~Item~, criteria: List~RankingCriteria~) List~RankedItem~
    }

    class SortBasedRankingStrategy {
        +rank(items: List~Item~, criteria: List~RankingCriteria~) List~RankedItem~
    }

    class ItemRepository {
        +save(item: Item) void
        +findById(id: String) Optional~Item~
        +findAll() List~Item~
        +updateScore(itemId: String, newScore: Double) void
    }

    class EventPublisher {
        +publishRankingUpdate(item: Item, oldScore: Double, newScore: Double) void
    }

    class RankingEvent {
        +itemId: String
        +oldScore: Double
        +newScore: Double
        +timestamp: Instant
    }

    RankingEngine --> Item : uses
    RankingEngine --> ScoreCalculator : delegates to
    RankingEngine --> RankingStrategy : uses
    ScoreCalculator --> Item : reads
    ScoreCalculator --> RankingCriteria : reads
    RankingStrategy --> SortBasedRankingStrategy : implements
    ItemRepository --> Item : persists
    EventPublisher --> RankingEvent : publishes

State Machine Diagram for Item

stateDiagram-v2
    [*] --> Active
    Active --> Inactive : score < threshold
    Inactive --> Active : score > threshold
    Active --> Deleted : softDelete()
    Deleted --> [*]

Service Class Structure and Responsibilities

The RankingEngine is the primary service responsible for:

  • Ranking items based on configured criteria
  • Updating item scores and publishing events
  • Managing ranking strategies and score calculations

Interface / Abstract Class Design

public interface RankingStrategy {
    List<RankedItem> rank(List<Item> items, List<RankingCriteria> criteria);
}

public interface ScoreCalculator {
    double calculateScore(Item item, List<RankingCriteria> criteria);
}

public enum CriteriaType {
    POPULARITY,
    RECENTNESS,
    ENGAGEMENT,
    CUSTOM
}

Business Logic Encapsulation Patterns

The ranking engine uses the Strategy pattern for different ranking approaches and the Factory pattern to create appropriate score calculators based on criteria type.

Critical-Path Method Implementation

public List<RankedItem> rankItems(List<Item> items, List<RankingCriteria> criteria) {
    // Transaction boundary - all operations in single DB transaction
    return transactionTemplate.execute(status -> {
        // 1. Calculate scores for all items
        List<RankedItem> rankedItems = new ArrayList<>();
        
        // 2. Apply scoring logic with proper locking to prevent race conditions
        for (Item item : items) {
            // Read current score with pessimistic locking if needed
            Item lockedItem = itemRepository.findByIdForUpdate(item.getId())
                .orElseThrow(() -> new ItemNotFoundException("Item not found: " + item.getId()));
            
            double newScore = scoreCalculator.calculateScore(lockedItem, criteria);
            
            // Update score in DB
            itemRepository.updateScore(lockedItem.getId(), newScore);
            
            // Publish event for async processing
            eventPublisher.publishRankingUpdate(lockedItem, lockedItem.getScore(), newScore);
            
            rankedItems.add(new RankedItem(lockedItem, newScore));
        }
        
        // 3. Sort items by calculated score in descending order
        rankedItems.sort(Comparator.comparing(RankedItem::getScore).reversed());
        
        // 4. Assign ranks (1-based)
        int rank = 1;
        for (RankedItem rankedItem : rankedItems) {
            rankedItem.setRank(rank++);
        }
        
        return rankedItems;
    });
}

Transaction Boundary Design

A single transaction encompasses:

  • Reading items with pessimistic locking to prevent race conditions
  • Calculating new scores
  • Updating item scores in DB
  • Publishing ranking update events

Service Interface Contracts

public interface RankingService {
    List<RankedItem> rankItems(List<String> itemIds, String criteriaId);
    void updateItemScore(String itemId, double newScore);
    List<RankedItem> getTopItems(int limit, String criteriaId);
}

Saga / Orchestration Pattern

For distributed transactions involving external systems (e.g., analytics), a saga pattern is used:

  • Each ranking operation triggers a saga
  • Sagas coordinate updates across multiple services (DB, cache, analytics)
  • Compensation logic handles rollback scenarios

Domain Object / Value Object Design

public class Item {
    private final String id;
    private final String name;
    private double score;
    private final Instant lastUpdated;
    private final Map<String, Object> metadata;
    
    // Constructor, getters, setters
}

public class RankedItem {
    private final Item item;
    private final int rank;
    private final double score;
    
    // Constructor, getters
}

public class RankingCriteria {
    private final String id;
    private final String name;
    private final double weight;
    private final CriteriaType type;
    private final Map<String, Object> config;
    
    // Constructor, getters
}

5. Concurrency & Thread Safety

Concurrency Model

The ranking engine will use a thread-based concurrency model with Java's java.util.concurrent package for managing parallel operations. The system will leverage a thread pool executor with configurable core and maximum pool sizes to handle concurrent ranking calculations and data updates. For I/O-bound operations like database queries and external API calls, the system will use non-blocking I/O patterns with CompletableFuture for asynchronous processing.

Race Condition Prevention Strategy

Race conditions will be prevented through:

  1. Atomic operations for counter updates using AtomicLong in ranking calculations
  2. Database-level atomic operations using SQL UPDATE ... SET ... WHERE with version checks
  3. Thread-local state management for temporary calculation contexts
  4. Synchronized access to shared mutable state in ranking algorithms with read-write locks

Optimistic vs. Pessimistic Locking Selection

Optimistic locking will be used for ranking calculations because:

  • Ranking updates are infrequent and typically involve small data sets
  • High concurrency is expected with multiple simultaneous ranking calculations
  • The system can tolerate occasional retries due to conflicts
  • Reduces lock contention and improves throughput

Implementation approach:

// Example optimistic locking in ranking update
public boolean updateRankingWithOptimisticLock(String entityId, int newScore) {
    int maxRetries = 3;
    for (int attempt = 0; attempt < maxRetries; attempt++) {
        try {
            // Read current state with version
            RankingEntity entity = repository.findById(entityId);
            int currentVersion = entity.getVersion();
            
            // Perform calculation
            entity.setScore(newScore);
            entity.setVersion(currentVersion + 1);
            
            // Update with version check
            boolean success = repository.updateWithVersionCheck(
                entity, currentVersion);
            
            if (success) {
                return true;
            }
            
        } catch (OptimisticLockException e) {
            if (attempt == maxRetries - 1) {
                throw new RetryableException("Failed to update ranking after " + maxRetries + " attempts", e);
            }
            Thread.sleep(100 * (1 << attempt)); // Exponential backoff
        }
    }
    return false;
}

Deadlock Prevention Strategy

Deadlock prevention will be achieved through:

  1. Consistent lock ordering - All ranking operations acquire locks in the same order (by entity ID hash)
  2. Timeout-based lock acquisition - 500ms timeout for lock acquisition to prevent indefinite blocking
  3. Hierarchical lock management - Locks acquired in a consistent hierarchy (global -> entity -> attribute)
  4. Lock monitoring - Periodic checks for long-held locks with alerts

Idempotency Implementation for Concurrent Duplicate Requests

Idempotency will be implemented using a deduplication key strategy:

public class IdempotencyService {
    private final RedisTemplate<String, String> redisTemplate;
    private static final String IDEMPOTENCY_PREFIX = "idempotency:";
    
    public boolean isIdempotent(String operationKey, String payloadHash) {
        String key = IDEMPOTENCY_PREFIX + operationKey;
        String existingHash = redisTemplate.opsForValue().get(key);
        
        if (existingHash != null && existingHash.equals(payloadHash)) {
            return true; // Already processed
        }
        
        // Store the operation hash for 1 hour
        redisTemplate.opsForValue().set(key, payloadHash, Duration.ofHours(1));
        return false;
    }
}

Thread Safety in Ranking Calculations

Ranking calculations will be made thread-safe through:

  1. Immutable calculation objects - All ranking algorithms will use immutable data structures
  2. Thread-local calculation contexts - Each thread maintains its own calculation state
  3. Synchronized access to shared configuration - Using java.util.concurrent.ConcurrentHashMap for caching algorithm parameters
  4. Atomic counters for performance metrics - Using AtomicLong for tracking calculation statistics

Concurrency Control in Batch Operations

For batch ranking updates:

public class BatchRankingProcessor {
    private final ExecutorService executorService = 
        Executors.newFixedThreadPool(10);
    
    public CompletableFuture<Void> processBatch(List<String> entityIds) {
        List<CompletableFuture<Void>> futures = new ArrayList<>();
        
        for (String entityId : entityIds) {
            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                try {
                    // Process each entity with proper error handling
                    processEntityRanking(entityId);
                } catch (Exception e) {
                    // Log and continue with other entities
                    logger.error("Failed to process entity ranking: {}", entityId, e);
                }
            }, executorService);
            
            futures.add(future);
        }
        
        return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
    }
}

Lock Granularity Strategy

The system will implement entity-level locking with:

  • Fine-grained locks for individual ranking entities to minimize contention
  • Shared read locks for concurrent read operations
  • Exclusive write locks for update operations
  • Lock timeout configuration to prevent indefinite waits

Memory Management Considerations

To ensure thread safety in memory:

  1. Immutable ranking result objects - All calculated rankings are immutable
  2. Thread-safe collections for intermediate calculation results
  3. Proper synchronization of shared mutable state
  4. Avoiding thread-local pollution through proper cleanup mechanisms

Performance Considerations

The concurrency model is designed to:

  • Maximize throughput for concurrent ranking calculations
  • Minimize lock contention through optimistic locking
  • Support horizontal scaling with proper thread pool sizing
  • Handle burst traffic gracefully with backpressure mechanisms

The design balances between high throughput, low latency, and system stability while maintaining data consistency through proper concurrency control mechanisms.

6. Sequence Diagrams for Key Flows

sequenceDiagram
    participant U as User
    participant API as API Gateway
    participant Auth as Auth Service
    participant Ranker as Ranking Engine
    participant DB as Database
    participant Cache as Redis Cache

    U->>API: GET /rankings?query=tech+news&limit=10
    API->>Auth: Validate JWT token
    Auth-->>API: Authenticated
    API->>Ranker: Process ranking request
    Ranker->>Cache: Check if results cached
    Cache-->>Ranker: Cache miss
    Ranker->>DB: Fetch relevant content items
    DB-->>Ranker: Content data
    Ranker->>DB: Fetch user preferences
    DB-->>Ranker: User preferences
    Ranker->>DB: Fetch user engagement data
    DB-->>Ranker: Engagement data
    Ranker->>Cache: Store ranked results
    Cache-->>Ranker: Cached
    Ranker-->>API: Return ranked content list
    API-->>U: 200 OK with ranked items
sequenceDiagram
    participant U as User
    participant API as API Gateway
    participant Auth as Auth Service
    participant Ranker as Ranking Engine
    participant DB as Database
    participant Cache as Redis Cache

    U->>API: POST /rankings/reindex
    API->>Auth: Validate admin token
    Auth-->>API: Authenticated
    API->>Ranker: Trigger reindexing
    Ranker->>DB: Fetch all content items
    DB-->>Ranker: Content data
    Ranker->>DB: Fetch all user engagement
    DB-->>Ranker: Engagement data
    Ranker->>DB: Update content scores
    DB-->>Ranker: Updated scores
    Ranker->>Cache: Invalidate all cached rankings
    Cache-->>Ranker: Invalidated
    Ranker-->>API: Reindexing completed
    API-->>U: 200 OK
sequenceDiagram
    participant U as User
    participant API as API Gateway
    participant Auth as Auth Service
    participant Ranker as Ranking Engine
    participant DB as Database
    participant Cache as Redis Cache
    participant EventBus as Message Broker

    U->>API: POST /content/123/rate
    API->>Auth: Validate JWT token
    Auth-->>API: Authenticated
    API->>Ranker: Rate content event
    Ranker->>DB: Update content score
    DB-->>Ranker: Updated
    Ranker->>Cache: Invalidate content cache
    Cache-->>Ranker: Invalidated
    Ranker->>EventBus: Publish content rated event
    EventBus-->>Ranker: Event published
    Ranker->>EventBus: Publish user engagement event
    EventBus-->>Ranker: Event published
    Ranker-->>API: Rate successful
    API-->>U: 200 OK

7. Caching Design

Cache Strategy Selection

For the ranking engine, we'll use a cache-aside pattern with write-through behavior for critical ranking data. This approach provides the best balance between consistency and performance for a system where freshness of rankings is important but not mission-critical for all reads.

Cache Key Design

Cache keys will follow a namespace strategy to avoid collisions and enable efficient invalidation:

ranking:{ranked_entity_type}:{entity_id}:{timestamp}:{sort_key}
ranking:leaderboard:{leaderboard_id}:{sort_key}:{time_window}
ranking:user_rank:{user_id}:{leaderboard_id}

TTL Strategy

Data TypeTTLReason
Leaderboard rankings5 minutesFrequent updates, acceptable staleness
User rank in leaderboard1 minuteHigh user interaction frequency
Individual entity scores30 secondsFast-changing scores
Aggregated stats10 minutesLess frequent updates

Cache Eviction Policy

We'll use LRU (Least Recently Used) eviction policy with a maxmemory policy set to allkeys-lru for Redis clusters. This ensures that the most frequently accessed ranking data stays in cache while maintaining memory efficiency.

Cache Stampede Prevention

To prevent cache stampedes:

  1. Probabilistic Early Expiry: Cache entries expire 20% earlier than TTL to allow for warm-up
  2. Distributed Locking: Use Redis distributed locks (SET key value NX EX ttl) for cache population operations
  3. Cache warming: Pre-warm hot leaderboards during low-traffic periods

Distributed Cache Architecture

We'll implement a Redis Cluster with:

  • 3 master nodes for write availability
  • 3 replica nodes per master for read scaling
  • Automatic failover with Redis Sentinel monitoring
  • Cross-zone replication for high availability

Cache Invalidation Strategy

We'll use event-driven invalidation:

  1. Write-through: When data changes, update cache immediately
  2. Event-driven invalidation:
    • Publish ranking.update events on score changes
    • Cache consumers listen to these events and invalidate relevant keys
  3. Time-based invalidation: TTLs ensure stale data is eventually cleaned up
@Component
public class RankingCacheManager {
    private final RedisTemplate<String, Object> redisTemplate;
    private final ObjectMapper objectMapper;
    
    public void invalidateUserRank(String userId, String leaderboardId) {
        String key = "ranking:user_rank:" + userId + ":" + leaderboardId;
        redisTemplate.delete(key);
    }
    
    public void invalidateLeaderboard(String leaderboardId, String sortKey) {
        String pattern = "ranking:leaderboard:" + leaderboardId + ":" + sortKey + ":*";
        Set<String> keys = redisTemplate.keys(pattern);
        redisTemplate.delete(keys);
    }
    
    public void invalidateEntityScore(String entityType, String entityId) {
        String pattern = "ranking:" + entityType + ":" + entityId + ":*";
        Set<String> keys = redisTemplate.keys(pattern);
        redisTemplate.delete(keys);
    }
}

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.

public interface RankingRepository {
    List<Ranking> findByUserId(String userId);
    Optional<Ranking> findById(String id);
    List<Ranking> findAll();
    Ranking save(Ranking ranking);
    void delete(String id);
    List<Ranking> findByCriteria(RankingCriteria criteria);
}

public class RankingRepositoryImpl implements RankingRepository {
    private final JdbcTemplate jdbcTemplate;
    private final ObjectMapper objectMapper;
    
    public RankingRepositoryImpl(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
        this.jdbcTemplate = jdbcTemplate;
        this.objectMapper = objectMapper;
    }
    
    @Override
    public List<Ranking> findByUserId(String userId) {
        String sql = "SELECT * FROM rankings WHERE user_id = ? ORDER BY created_at DESC";
        return jdbcTemplate.query(sql, new Object[]{userId}, new RankingRowMapper());
    }
    
    @Override
    public Optional<Ranking> findById(String id) {
        String sql = "SELECT * FROM rankings WHERE id = ?";
        try {
            Ranking ranking = jdbcTemplate.queryForObject(sql, new Object[]{id}, new RankingRowMapper());
            return Optional.of(ranking);
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }
    
    @Override
    public List<Ranking> findAll() {
        String sql = "SELECT * FROM rankings ORDER BY created_at DESC";
        return jdbcTemplate.query(sql, new RankingRowMapper());
    }
    
    @Override
    public Ranking save(Ranking ranking) {
        if (ranking.getId() == null) {
            String id = UUID.randomUUID().toString();
            ranking.setId(id);
            insertRanking(ranking);
        } else {
            updateRanking(ranking);
        }
        return ranking;
    }
    
    private void insertRanking(Ranking ranking) {
        String sql = """
            INSERT INTO rankings (id, user_id, score, metadata, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, ?)
            """;
        jdbcTemplate.update(sql, 
            ranking.getId(),
            ranking.getUserId(),
            ranking.getScore(),
            objectMapper.writeValueAsString(ranking.getMetadata()),
            ranking.getCreatedAt(),
            ranking.getUpdatedAt()
        );
    }
    
    private void updateRanking(Ranking ranking) {
        String sql = """
            UPDATE rankings 
            SET score = ?, metadata = ?, updated_at = ?
            WHERE id = ?
            """;
        jdbcTemplate.update(sql,
            ranking.getScore(),
            objectMapper.writeValueAsString(ranking.getMetadata()),
            ranking.getUpdatedAt(),
            ranking.getId()
        );
    }
    
    @Override
    public void delete(String id) {
        String sql = "DELETE FROM rankings WHERE id = ?";
        jdbcTemplate.update(sql, id);
    }
    
    @Override
    public List<Ranking> findByCriteria(RankingCriteria criteria) {
        StringBuilder sql = new StringBuilder("SELECT * FROM rankings WHERE 1=1");
        List<Object> params = new ArrayList<>();
        
        if (criteria.getUserId() != null) {
            sql.append(" AND user_id = ?");
            params.add(criteria.getUserId());
        }
        
        if (criteria.getScoreRange() != null) {
            sql.append(" AND score BETWEEN ? AND ?");
            params.add(criteria.getScoreRange().getMin());
            params.add(criteria.getScoreRange().getMax());
        }
        
        sql.append(" ORDER BY created_at DESC");
        
        return jdbcTemplate.query(sql.toString(), params.toArray(), new RankingRowMapper());
    }
}

ORM vs Raw Query Decisions

For the ranking engine, raw SQL queries are preferred over ORM for performance-critical operations:

  1. Performance: Raw queries provide better control over query execution and can be optimized for specific use cases
  2. Complex aggregations: Ranking calculations often require complex SQL operations that are hard to express with ORM
  3. Database-specific features: We leverage PostgreSQL window functions and materialized views for ranking calculations

However, simple CRUD operations use ORM for developer productivity:

// For simple operations - using JPA/Hibernate
@PersistenceContext
private EntityManager entityManager;

public List<Ranking> findRecentByUserId(String userId) {
    return entityManager.createQuery(
        "SELECT r FROM Ranking r WHERE r.userId = :userId ORDER BY r.createdAt DESC", 
        Ranking.class)
        .setParameter("userId", userId)
        .setMaxResults(100)
        .getResultList();
}

Read vs Write Separation

The ranking engine follows CQRS pattern for read and write operations:

public class ReadRankingRepository {
    private final RedisTemplate<String, String> redisTemplate;
    
    public ReadRankingRepository(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    public List<Ranking> getTopRankings(int limit) {
        Set<String> rankingKeys = redisTemplate.opsForZSet().range("rankings:leaderboard", 0, limit - 1);
        return rankingKeys.stream()
            .map(key -> {
                String json = redisTemplate.opsForValue().get(key);
                return parseRankingJson(json);
            })
            .collect(Collectors.toList());
    }
    
    public void updateLeaderboard(Ranking ranking) {
        String key = "ranking:" + ranking.getId();
        String value = objectMapper.writeValueAsString(ranking);
        
        // Update in Redis with sorted set for leaderboards
        redisTemplate.opsForZSet().add("rankings:leaderboard", key, ranking.getScore());
        redisTemplate.opsForValue().set(key, value, 30, TimeUnit.DAYS);
    }
}

Connection Pool Configuration

Connection pool is configured with optimal settings for high-concurrency ranking engine:

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

N+1 Query Prevention Strategy

To prevent N+1 queries, eager loading and batch operations are implemented:

// Using @BatchSize annotation for batch loading
@OneToMany(mappedBy = "ranking", fetch = FetchType.LAZY)
@BatchSize(size = 20)
private List<RankingEntry> entries;

// Custom batch query for ranking entries
public List<RankingEntry> findEntriesForRankings(List<String> rankingIds) {
    String sql = "SELECT * FROM ranking_entries WHERE ranking_id IN (" + 
        rankingIds.stream().map(id -> "'"+id+"'").collect(Collectors.joining(",")) + ")";
    return jdbcTemplate.query(sql, new RankingEntryRowMapper());
}

Batch Operation Design

Batch operations are implemented for bulk ranking updates:

public class BatchRankingService {
    private final JdbcTemplate jdbcTemplate;
    
    public void batchUpdateRankings(List<RankingUpdateRequest> updates) {
        String sql = "UPDATE rankings SET score = ?, updated_at = ? WHERE id = ?";
        
        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                RankingUpdateRequest update = updates.get(i);
                ps.setDouble(1, update.getScore());
                ps.setTimestamp(2, Timestamp.valueOf(LocalDateTime.now()));
                ps.setString(3, update.getId());
            }
            
            @Override
            public int getBatchSize() {
                return updates.size();
            }
        });
    }
}

Data Access Layer Design Summary

The repository layer is designed with:

  1. Separation of concerns: Clear distinction between read and write operations
  2. Performance optimization: Raw SQL for complex queries, ORM for simple CRUD
  3. Scalability: Connection pooling and batch operations for high throughput
  4. Maintainability: Clean interfaces with proper error handling and logging
  5. Flexibility: Support for both relational and NoSQL data stores through abstraction

9. Asynchronous Processing & Messaging

Event-driven boundaries in this feature

The ranking engine will process content items and update their scores asynchronously to avoid blocking user requests. The key async boundaries are:

  1. Content creation/update → triggers ranking recalculation
  2. User interaction events (likes, comments, shares) → trigger content score adjustments
  3. Periodic re-rankings → batch updates for content that hasn't been recently interacted with

Synchronous operations include:

  • Real-time score retrieval for a specific content item
  • Manual rank adjustment by admin

Message schema design

{
  "version": "1.0",
  "eventType": "content_ranking_update",
  "payload": {
    "contentId": "uuid",
    "score": 87.5,
    "timestamp": "2023-10-27T10:30:00Z",
    "reason": "user_interaction",
    "interactionType": "like"
  },
  "metadata": {
    "source": "user-service",
    "eventId": "uuid",
    "correlationId": "uuid"
  }
}

Dead letter queue strategy and poison message handling

Messages that fail processing more than 3 times will be moved to a dead letter queue (DLQ) with:

  • A 24-hour delay before retry
  • Metadata retention for debugging (original message, error details, failure count)
  • Alerting to on-call team when DLQ threshold is exceeded (5+ messages)

Poison message handling:

  • Messages that consistently fail due to malformed data will be flagged for manual review
  • Retry logic includes exponential backoff with jitter (1s, 2s, 4s, 8s max)

At-least-once vs. exactly-once delivery

At-least-once delivery is selected because:

  • Content scores are idempotent - multiple updates to same content with same score don't change the result
  • The system uses a combination of content ID + timestamp to ensure duplicate events don't cause incorrect score adjustments
  • The cost of ensuring exactly-once (complex state tracking) outweighs the benefit for this use case

Retry policy for consumers

Retry configuration:

  • Max attempts: 3
  • Backoff formula: exponential with jitter (1s + random(0,1s), 2s + random(0,1s), 4s + random(0,1s))
  • Non-retryable errors: malformed content ID, invalid score format
  • Retryable errors: network timeouts, database connection issues

Outbox pattern for reliable event publishing

The outbox pattern is implemented to ensure message delivery reliability:

public class OutboxEvent {
    private String id;
    private String eventType;
    private String payload;
    private String status; // PENDING, SENT, FAILED
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

public class EventPublisher {
    private final EventRepository eventRepository;
    private final KafkaTemplate<String, String> kafkaTemplate;

    public void publishEvent(String eventType, String payload) {
        // 1. Save to outbox table
        OutboxEvent event = new OutboxEvent();
        event.setId(UUID.randomUUID().toString());
        event.setEventType(eventType);
        event.setPayload(payload);
        event.setStatus("PENDING");
        eventRepository.save(event);

        // 2. Publish to Kafka
        try {
            kafkaTemplate.send("ranking-events", event.getId(), payload);
            event.setStatus("SENT");
        } catch (Exception e) {
            event.setStatus("FAILED");
            // Log and trigger DLQ handling
        }
        eventRepository.save(event);
    }
}

Sequence diagram for async flows

sequenceDiagram
    participant User
    participant ContentService
    participant RankingEngine
    participant Kafka
    participant EventProcessor

    User->>ContentService: Create content
    ContentService->>Kafka: Publish content_created event
    Kafka->>EventProcessor: Deliver content_created event
    EventProcessor->>RankingEngine: Trigger rank calculation
    RankingEngine->>RankingEngine: Calculate new score
    RankingEngine->>Kafka: Publish content_ranked event
    Kafka->>ContentService: Deliver content_ranked event
    ContentService->>User: Return updated content with rank

10. Error Handling Design

Error Classification Hierarchy

The ranking engine follows a three-tier error classification system:

  1. Domain Errors - Business logic violations (e.g., invalid ranking criteria, missing required fields)
  2. Infrastructure Errors - System-level issues (e.g., database connection failures, cache timeouts)
  3. Validation Errors - Input parameter issues (e.g., malformed query parameters, out-of-range scores)
public enum ErrorType {
    VALIDATION_ERROR,
    DOMAIN_ERROR,
    INFRASTRUCTURE_ERROR
}

Error Propagation Strategy

Errors are propagated up the call stack with appropriate wrapping to maintain context:

public class RankingEngineException extends RuntimeException {
    private final ErrorType errorType;
    private final String errorCode;
    
    public RankingEngineException(ErrorType errorType, String errorCode, String message) {
        super(message);
        this.errorType = errorType;
        this.errorCode = errorCode;
    }
    
    public RankingEngineException(ErrorType errorType, String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorType = errorType;
        this.errorCode = errorCode;
    }
    
    // Getters
}

Retry Strategy with Exponential Backoff and Jitter

For infrastructure errors, a retry mechanism is implemented with exponential backoff and jitter:

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 (true) {
            try {
                return operation.get();
            } catch (RankingEngineException e) {
                if (attempts >= MAX_RETRIES || !isRetryable(e)) {
                    throw e;
                }
                long delay = calculateDelay(attempts);
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RankingEngineException(
                        ErrorType.INFRASTRUCTURE_ERROR, 
                        "RETRY_INTERRUPTED", 
                        "Retry interrupted", 
                        ie
                    );
                }
                attempts++;
            }
        }
    }
    
    private boolean isRetryable(RankingEngineException e) {
        return e.getErrorType() == ErrorType.INFRASTRUCTURE_ERROR;
    }
    
    private long calculateDelay(int attempt) {
        long baseDelay = (long) Math.pow(2, attempt) * BASE_DELAY_MS;
        long jitter = (long) (baseDelay * JITTER_FACTOR * (Math.random() - 0.5) * 2);
        return baseDelay + jitter;
    }
}

Circuit Breaker Design

For external dependencies and high-traffic operations, a circuit breaker is implemented:

public class CircuitBreaker {
    private final int failureThreshold;
    private final long timeout;
    private final int successThreshold;
    
    private volatile CircuitState state = CircuitState.CLOSED;
    private int failureCount = 0;
    private 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();
            throw new RankingEngineException(
                ErrorType.INFRASTRUCTURE_ERROR, 
                "CIRCUIT_OPEN", 
                "Circuit breaker is open"
            );
        }
    }
    
    private <T> T executeOpen(Supplier<T> operation) {
        if (System.currentTimeMillis() - lastFailureTime > timeout) {
            state = CircuitState.HALF_OPEN;
            return executeHalfOpen(operation);
        }
        throw new RankingEngineException(
            ErrorType.INFRASTRUCTURE_ERROR, 
            "CIRCUIT_OPEN", 
            "Circuit breaker 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 RankingEngineException(
                ErrorType.INFRASTRUCTURE_ERROR, 
                "CIRCUIT_OPEN", 
                "Circuit breaker is open"
            );
        }
    }
    
    private void onSuccess() {
        failureCount = 0;
    }
    
    private void onFailure() {
        failureCount++;
        lastFailureTime = System.currentTimeMillis();
        if (failureCount >= failureThreshold) {
            state = CircuitState.OPEN;
        }
    }
}

Fallback Behavior Design

For critical operations, fallback mechanisms are implemented:

public class RankingFallback {
    public List<RankedItem> getRankingsWithFallback(
        String criteria, 
        List<String> items,
        Supplier<List<RankedItem>> primaryOperation
    ) {
        try {
            return primaryOperation.get();
        } catch (RankingEngineException e) {
            // Log the error for monitoring
            logger.warn("Primary ranking operation failed, using fallback", e);
            
            // Return default ranked items or empty list based on error type
            if (e.getErrorType() == ErrorType.VALIDATION_ERROR) {
                return Collections.emptyList();
            }
            
            // For infrastructure errors, attempt to return cached results or default ranking
            return fallbackToCachedRankings(criteria);
        }
    }
    
    private List<RankedItem> fallbackToCachedRankings(String criteria) {
        // Implementation depends on cache layer design
        return Collections.emptyList();
    }
}

Error Logging Format

Structured logging follows a consistent schema:

public class ErrorLogFormatter {
    public static String formatError(
        String operation, 
        String userId, 
        String entityId, 
        String errorCode, 
        String errorMessage,
        Exception exception
    ) {
        return String.format(
            "{\"operation\":\"%s\",\"userId\":\"%s\",\"entityId\":\"%s\",\"errorCode\":\"%s\"," +
            "\"errorMessage\":\"%s\",\"stackTrace\":\"%s\",\"timestamp\":\"%s\"}",
            operation, userId, entityId, errorCode, 
            escapeJson(errorMessage), escapeJson(exception.toString()),
            Instant.now().toString()
        );
    }
    
    private static String escapeJson(String input) {
        return input.replace("\\", "\\\\").replace("\"", "\\\"");
    }
}

Alerting Thresholds

Critical error thresholds trigger alerts:

Error TypeAlert LevelThresholdAlert Channel
Infrastructure errorsCritical>100 occurrences/minuteOn-call
Domain errorsWarning>50 occurrences/minuteSlack
Validation errorsInfo>1000 occurrences/hourEmail

Error Budget Considerations

The ranking engine operates within a defined error budget:

public class ErrorBudgetManager {
    private static final double ERROR_BUDGET_PERCENTAGE = 0.05; // 5% of total requests
    
    public boolean isWithinErrorBudget(int totalRequests, int errorCount) {
        double errorRate = (double) errorCount / totalRequests;
        return errorRate <= ERROR_BUDGET_PERCENTAGE;
    }
    
    public void handleErrorBudgetExceeded() {
        // Trigger alerting and potentially pause new ranking operations
        logger.error("Error budget exceeded - triggering alert");
    }
}

11. Security Design

Input Validation and Sanitization Strategy

All API inputs undergo strict validation using Apache Commons Validator and custom regex patterns for:

  • Numeric ranges (e.g., rank limits 1-1000)
  • String length constraints (max 255 chars for titles)
  • Allowed character sets (alphanumeric + spaces + hyphens for names)

SQL Injection Prevention

All database queries use parameterized prepared statements exclusively. No dynamic query construction or string concatenation. ORM framework (Hibernate) is configured with strict validation mode to prevent any raw SQL injection vectors.

Authentication Middleware Design

Custom JWT-based authentication middleware with:

  • Token expiration handling (1-hour TTL)
  • Refresh token rotation (30-day validity)
  • Token revocation via Redis blacklist with 1-hour TTL
  • Middleware intercepts all requests and validates JWT signature, expiration, and audience claims

Authorization Enforcement

Role-Based Access Control (RBAC) with:

  • Admin role: Full CRUD access to all rankings and entities
  • Editor role: Read/write access to owned rankings only
  • Viewer role: Read-only access to public rankings
  • Permission checks performed at service layer using Spring Security annotations (@PreAuthorize)
  • User context propagation through SecurityContext and Authentication objects

Sensitive Data Encryption

At Rest:

  • AES-256 GCM encryption for sensitive ranking data (metadata, user preferences)
  • Key rotation every 90 days using AWS KMS
  • Encrypted field annotations on domain objects (e.g., @Encrypted)

In Transit:

  • TLS 1.3 with HSTS header enforcement
  • All API endpoints require HTTPS (redirect from HTTP)
  • Certificate pinning for external service integrations

PII Handling and Masking Strategy

  • User identifiers (user IDs, IP addresses) are hashed using SHA-256 with salt
  • Personal names are masked in logs and audit trails (first letter + asterisks)
  • Audit trail entries contain only hashed user IDs, not raw identifiers
  • GDPR-compliant data retention policy (6 months for logs, 1 year for user preferences)

Secrets Management

  • AWS Secrets Manager integration for:
    • Database credentials (rotated every 30 days)
    • JWT signing keys (rotated every 90 days)
    • External service API keys
  • Secrets are fetched at application startup and cached with automatic refresh on expiration
  • No hardcoded secrets in source code or configuration files

Audit Logging Design

Events Logged:

  • Ranking creation/deletion (user, timestamp, action)
  • Rank modifications (before/after values, change reason)
  • User access to rankings (user ID, timestamp, action type)

Format:

{
  "timestamp": "2023-10-15T14:30:00Z",
  "userId": "hashed_user_id",
  "action": "RANKING_CREATE",
  "entityId": "uuid-12345",
  "details": {
    "rankingName": "hashed_name",
    "modifiedFields": ["rank", "score"]
  }
}

Retention:

  • 1 year for audit logs
  • 30 days for error logs and security events
  • Logs stored in secure S3 bucket with encryption at rest
// Example of input validation in service layer
public class RankingService {
    private static final Pattern VALID_RANKING_NAME = Pattern.compile("^[a-zA-Z0-9\\s\\-]{1,255}$");
    
    public void createRanking(RankingRequest request) {
        // Validate input
        if (!VALID_RANKING_NAME.matcher(request.getName()).matches()) {
            throw new InvalidInputException("Invalid ranking name format");
        }
        
        // Sanitize input
        String sanitized = sanitizeInput(request.getName());
        
        // Validate numeric ranges
        if (request.getRank() < 1 || request.getRank() > 1000) {
            throw new InvalidInputException("Rank must be between 1 and 1000");
        }
        
        // Proceed with creation...
    }
    
    private String sanitizeInput(String input) {
        return input.replaceAll("[^a-zA-Z0-9\\s\\-]", "");
    }
}

12. Testing Strategy

Unit Testing Scope and Coverage Targets

  • Target coverage: 85% line coverage, 90% branch coverage for core business logic
  • Scope: All service classes, domain objects, value objects, and utility functions
  • Testing approach:
    • Mock external dependencies (database, cache, messaging systems)
    • Test edge cases and boundary conditions for ranking calculations
    • Validate business rule implementations with parameterized test cases

Integration Testing Strategy

  • Database integration:
    • Use testcontainers for PostgreSQL/MySQL instances to validate actual query execution and schema compliance
    • Test transaction boundaries and data consistency across multiple operations
  • External service integration:
    • WireMock or equivalent for testing interactions with external ranking providers (e.g., Elasticsearch, Solr)
    • Validate API contract compliance and error handling for external service failures
  • End-to-end flow testing:
    • Test complete ranking pipeline from data ingestion to result retrieval
    • Validate cache interaction and consistency between database and cache layers

Contract Testing

  • Framework: Pact for consumer-driven contract testing
  • Scope:
    • Define contracts between ranking service and downstream consumers (e.g., search, recommendation services)
    • Validate that the API response structure matches consumer expectations
  • Versioning: Contract version alignment with API versioning strategy

Performance Testing Tool and Key Scenarios

  • Tool: k6 for load testing and performance benchmarking
  • Key scenarios:
    • Concurrent ranking requests (1000+ concurrent users)
    • High-volume data ingestion and recalculation workflows
    • Latency-sensitive ranking operations (p95 < 200ms, p99 < 500ms)
  • Metrics tracked:
    • Request rate, response time distribution, error rates
    • Throughput for batch ranking operations

Test Data Management Strategy

  • Fixtures:
    • Predefined test data sets for common ranking scenarios (e.g., trending, popularity, recency)
    • Use JSON/YAML fixtures for consistent data across test environments
  • Factories:
    • Implement factory pattern for generating realistic test entities (users, items, rankings)
    • Support for generating large-scale synthetic data sets for performance testing
  • Seeding:
    • Automated test data setup using database migration scripts with test-specific seed data
    • Ensure test isolation by cleaning up test data after each test run
// Example unit test for ranking calculation logic
@Test
public void testRankingCalculationWithMultipleFactors() {
    // Given
    List<RankingFactor> factors = Arrays.asList(
        new RankingFactor("popularity", 0.4),
        new RankingFactor("recency", 0.3),
        new RankingFactor("engagement", 0.3)
    );
    
    Item item = Item.builder()
        .id("item-1")
        .popularity(95)
        .recency(2)
        .engagement(80)
        .build();
    
    // When
    double score = rankingService.calculateRankingScore(item, factors);
    
    // Then
    assertThat(score).isEqualTo(85.0); // Expected weighted score calculation
}

13. Observability Design

Structured Logging Schema

All logs must follow a consistent schema to enable effective querying and analysis:

{
  "timestamp": "2023-10-27T14:30:45.123Z",
  "level": "INFO|WARN|ERROR",
  "service": "ranking-engine",
  "trace_id": "12345678-1234-1234-1234-123456789012",
  "span_id": "abcdef12-3456-7890-abcd-ef1234567890",
  "user_id": "user_12345",
  "entity_id": "rankable_item_98765",
  "operation": "calculateRanking",
  "request_id": "req_abc123",
  "duration_ms": 45,
  "error_code": "RANKING_CALCULATION_FAILED",
  "error_message": "Failed to calculate rank for item due to invalid weight",
  "payload": {
    "item_id": "item_123",
    "weights": {"popularity": 0.7, "recency": 0.3}
  },
  "pii_masked": true
}

PII fields (user_id, email, etc.) must be masked in logs using a consistent pattern: *** or hash-based obfuscation. The logging framework should support structured logging with contextual fields and automatic trace propagation.

Distributed Tracing Instrumentation

OpenTelemetry is used for distributed tracing with custom spans:

Key Operations to Trace:

  • calculateRanking - Main ranking calculation operation
  • fetchItemMetadata - Fetches item metadata for scoring
  • applyWeights - Applies weight calculations to items
  • persistRankings - Saves calculated rankings to database

Span Attributes:

// Example span attributes for calculateRanking operation
span.setAttribute("ranking_engine.operation", "calculateRanking");
span.setAttribute("ranking_engine.item_count", items.size());
span.setAttribute("ranking_engine.algorithm", "weighted_score");
span.setAttribute("ranking_engine.cache_hit", false);

Custom Spans:

  • RankingCalculationSpan - Wraps entire ranking calculation workflow
  • ItemScoringSpan - Per-item scoring operation
  • DatabaseQuerySpan - Database query operations with query plan

Metrics Design

RED Method Metrics (Rate, Errors, Duration):

Metric NameTypeLabelsDescription
ranking_calculations_totalCounteroperation, statusTotal ranking calculations
ranking_calculations_errorsCounteroperation, error_typeFailed ranking calculations
ranking_calculation_duration_msHistogramoperationDuration of ranking calculations
cache_hits_totalCountercache_type, hit_typeCache hit statistics
database_query_duration_msHistogramquery_type, statusDatabase query latencies

Additional Service-Level Metrics:

  • active_ranking_jobs - Gauge, tracks concurrent ranking jobs
  • cache_hit_rate - Gauge, calculated from cache metrics
  • queue_depth - Gauge, tracks pending ranking calculation jobs
  • item_processing_rate - Counter, items processed per second

Health Check Endpoint Design

Endpoint Structure:

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

Response Format:

{
  "status": "healthy|unhealthy",
  "timestamp": "2023-10-27T14:30:45.123Z",
  "service": "ranking-engine",
  "checks": {
    "database": {
      "status": "healthy",
      "latency_ms": 12
    },
    "cache": {
      "status": "healthy",
      "latency_ms": 5
    },
    "dependencies": {
      "status": "healthy",
      "details": ["auth-service:healthy", "notification-service:healthy"]
    }
  },
  "version": "1.2.3",
  "uptime_seconds": 3600
}

Check Types:

  • /health - Basic service health, checks core dependencies
  • /ready - Service ready for traffic, includes database connectivity and cache status
  • /live - Process alive check, basic liveness probe
  • /startup - Startup validation, validates configuration and initialization

Alerting Thresholds

Critical Alerts (Escalation to On-Call):

MetricThresholdAlert ConditionEscalation
ranking_calculations_errors> 5 errors/minuteError rate exceeds threshold for 5 min
ranking_calculation_duration_ms> 500ms p99P99 latency exceeds threshold
database_query_duration_ms> 1000ms p99DB query latency exceeds threshold
cache_hit_rate< 80%Cache hit rate drops below threshold

Warning Alerts (Monitoring):

MetricThresholdAlert Condition
ranking_calculation_duration_ms> 200ms p95P95 latency exceeds threshold
cache_hit_rate< 90%Cache hit rate drops below threshold
queue_depth> 100Pending jobs exceed threshold

On-Call Escalation Path:

  • Critical alerts → On-call engineer → Slack alert + email
  • Warning alerts → Monitoring team → Slack notification

Log Rotation and Retention

  • Log files: Rotate every 24 hours or 100MB
  • Retention policy: 30 days for operational logs, 90 days for audit logs
  • Compression: GZIP compression for archived logs
  • Centralized logging: Send to ELK stack or cloud logging service with search capabilities

Performance Monitoring

  • CPU usage: Alert if > 85% for more than 5 minutes
  • Memory usage: Alert if > 90% of allocated heap
  • Thread pool utilization: Alert if > 95% of max threads in use
  • Database connection pool: Alert if > 90% of connections in use

Audit Trail Integration

All ranking-related operations must be logged with:

  • User context (if available)
  • Timestamps
  • Operation type and parameters
  • Results and status
  • Error details for failed operations

This observability design enables comprehensive monitoring, rapid incident response, and performance optimization for the ranking engine system.

14. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering alignment

  • Ranking algorithm selection: Should we use a simple weighted score system or complex machine learning models? This decision will impact both development timeline and operational complexity.
  • Real-time vs. batch ranking: How frequently should rankings be updated? Real-time updates increase system complexity and latency, while batch processing introduces staleness.
  • Ranking scope: Should rankings be global or scoped to specific user segments, regions, or contexts? This affects data model complexity and caching strategy.
  • Personalization depth: How much personalization should be applied to rankings? Deep personalization increases system complexity and requires more training data.

Known trade-offs and justifications

Trade-offOption 1Option 2Justification
Consistency vs. PerformanceStrong consistency with distributed locksEventual consistency with optimistic concurrencyFor a ranking engine, users may tolerate slight staleness for better performance and scalability
Complexity vs. MaintainabilityComplex ML-based ranking algorithmsSimple weighted scoring systemSimpler systems are easier to debug, maintain, and scale
Real-time vs. Batch UpdatesReal-time ranking updatesBatch-based updates with hourly granularityBatch processing reduces system load and improves throughput
Storage vs. ComputeStore pre-computed rankingsCompute rankings on-demandPre-computed rankings improve read performance but increase storage requirements

Risks and mitigation strategies

  • Ranking algorithm bias: Risk of systematic favoritism toward certain content categories or user segments

    • Mitigation: Regular algorithm audits, A/B testing with diverse user groups, bias detection mechanisms
  • Cache stampede: High concurrent requests causing cache misses and database overload

    • Mitigation: Implement probabilistic early expiration, distributed locking for cache warming, request queuing
  • Data inconsistency: Rankings becoming stale due to concurrent updates

    • Mitigation: Use optimistic locking with version checks, implement proper transaction boundaries, cache invalidation strategies
  • Scalability bottlenecks: Ranking engine becoming a single point of failure under high load

    • Mitigation: Horizontal sharding by content category, read replicas for ranking computation, circuit breaker patterns

Technical debt acknowledged

  • Algorithm evolution complexity: Future modifications to ranking algorithms may require significant refactoring of existing systems
  • Monitoring gap: Current design lacks comprehensive metrics for ranking quality and user engagement tracking
  • Personalization limitations: Initial implementation may not support advanced personalization features that could be added later
  • Eventual consistency: The system's eventual consistency model may lead to inconsistent user experiences in high-concurrency scenarios
  • Data freshness: Batch processing introduces latency that may impact user experience for time-sensitive content ranking

Was this article helpful?

Topics in this article

More from Backend LLD

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.