Skip to main content

Spell Checker System Design

Comprehensive low-level design for a scalable spell checking system with autocomplete and fuzzy matching capabilities.

A spell checker system that efficiently validates word spelling while providing autocomplete and fuzzy matching features through optimized data structures and algorithms.

1. Problem Statement & Requirements

Clarifying questions

  1. What scale expectations are there for the spell checker? Will it handle millions of requests per second, or is it more for a small to medium business use case?
  2. What consistency requirements exist? Should the spell checker return real-time corrections, or can it tolerate slight delays for better accuracy?
  3. What are the key entities and their relationships? Are we building a spell checker for a single document, or supporting multiple documents with shared dictionaries and user preferences?
  4. What is the read/write ratio? Is this primarily a lookup service, or does it involve frequent dictionary updates?
  5. What latency sensitivity is acceptable? Should corrections happen in <10ms, or is a few hundred milliseconds acceptable?
  6. What authentication model is expected? Will the spell checker be used internally or exposed via an API with OAuth2/JWT?
  7. Are there any external integrations? Does it need to integrate with third-party dictionaries or language models?

Functional requirements

  • Accept text input and return corrected spelling
  • Support multiple languages (English, Spanish, French, etc.)
  • Provide suggestions for misspelled words
  • Allow users to add new words to personal dictionaries
  • Support batch processing of documents
  • Maintain a global dictionary and user-specific dictionaries
  • Allow users to set preferred language for correction

Non-functional requirements

  • Latency targets: p50: <50ms, p95: <100ms, p99: <200ms for single word corrections
  • Throughput: 10,000+ requests per second for batch processing, 5,000+ for real-time
  • Availability: 99.9% uptime (99.99% for premium users)
  • Scalability: Support horizontal scaling across multiple instances
  • Accuracy: >95% accuracy for common words and phrases

Constraints and assumptions

  • Assumption: Spell checking will primarily be used for English language content
  • Constraint: Dictionary size should not exceed 50GB to maintain performance
  • Assumption: Users will have a limited set of personal dictionaries (max 1000 words)
  • Constraint: API should not exceed 2MB payload size for single requests
  • Assumption: Spell checker will be used within a larger application (e.g., word processor, web editor)

Out of scope

  • Real-time collaborative spell checking (multi-user editing)
  • Machine learning model training or retraining
  • Integration with external language services like Google Translate or Microsoft Translator
  • Spell checking for non-text content (images, voice)
  • Support for real-time collaborative editing or version control
  • Spell checking in multiple languages simultaneously (e.g., mixed content)
  • Integration with external dictionaries or language models beyond basic word lists

2. API Design

API Style Selection

For spell checking, I'll select REST API with JSON payloads for the following reasons:

  • Simplicity: REST is well-understood and widely adopted, making it easier for developers to integrate
  • Statelessness: Spell checking is a stateless operation, making REST ideal for this use case
  • Caching: REST supports HTTP caching mechanisms which can be leveraged for frequently checked words
  • Tooling: Rich ecosystem of REST clients, testing tools, and documentation generators

Endpoint Definitions

MethodPathDescriptionAuth
POST/v1/spell-checkSpell check a single word or textJWT
POST/v1/spell-check/batchSpell check multiple words or texts in batchJWT
GET/v1/suggestions/{word}Get spelling suggestions for a wordJWT
GET/v1/suggestions/batchGet suggestions for multiple words in batchJWT
POST/v1/word/addAdd a word to custom dictionaryJWT
DELETE/v1/word/remove/{word}Remove a word from custom dictionaryJWT

Request and Response Schemas

Spell Check Single Word

{
  "word": "recieve",
  "context": "I recieve many emails"
}
{
  "word": "recieve",
  "isCorrect": false,
  "suggestions": ["receive", "receiving", "recieved"],
  "context": "I recieve many emails",
  "matchedWord": "recieve"
}

Spell Check Batch

{
  "words": ["recieve", "teh", "accomodate"],
  "context": "I recieve many emails from teh office and accomodate guests"
}
{
  "results": [
    {
      "word": "recieve",
      "isCorrect": false,
      "suggestions": ["receive", "receiving", "recieved"],
      "matchedWord": "recieve"
    },
    {
      "word": "teh",
      "isCorrect": false,
      "suggestions": ["the", "they", "their"],
      "matchedWord": "teh"
    }
  ],
  "context": "I recieve many emails from teh office and accomodate guests"
}

Suggestions Endpoint

{
  "word": "recieve",
  "maxSuggestions": 5,
  "distance": 2
}
{
  "word": "recieve",
  "suggestions": ["receive", "receiving", "recieved"],
  "distance": 2
}

HTTP Status Codes and Error Response Format

CodeDescription
200Success
400Bad Request (malformed input)
401Unauthorized
403Forbidden
429Rate Limited
500Internal Server Error

Standardized error response:

{
  "error": {
    "code": "INVALID_INPUT",
    "message": "Word must be between 1 and 100 characters",
    "details": {
      "field": "word",
      "value": "a"
    }
  }
}

API Versioning Strategy

Versioning will be done via path prefix: /v1/... with support for future versioning like /v2/.... This approach is simple, explicit, and allows for clear migration paths.

Pagination Design

For batch operations, we'll use cursor-based pagination because:

  • It's more stable across data changes
  • Provides better performance for large result sets
  • Handles concurrent modifications gracefully

Authentication and Authorization

All endpoints require JWT-based authentication with scopes:

  • spellcheck:read - for read operations
  • spellcheck:write - for write operations (add/remove words)

Rate Limiting

Rate limiting will be implemented per endpoint:

  • POST /v1/spell-check: 1000 req/min
  • POST /v1/spell-check/batch: 500 req/min
  • GET /v1/suggestions/{word}: 2000 req/min
  • POST /v1/word/add: 50 req/min

Rate limiting will be enforced using Redis with sliding window algorithm to prevent burst attacks.

Idempotency Design

For mutation endpoints (POST /v1/word/add, DELETE /v1/word/remove/{word}), we'll implement idempotency using:

  • Idempotency-Key header: X-Idempotency-Key: <uuid>
  • Storage: Store the operation result in Redis with TTL of 1 hour
  • Behavior: If same key is used, return the stored result instead of reprocessing
// Example idempotency check in service layer
public WordResult addWord(String word, String idempotencyKey) {
    String cacheKey = "idempotency:" + idempotencyKey;
    
    // Check if operation already completed
    String cachedResult = redisTemplate.opsForValue().get(cacheKey);
    if (cachedResult != null) {
        return objectMapper.readValue(cachedResult, WordResult.class);
    }
    
    // Process the operation
    WordResult result = processAddWord(word);
    
    // Cache the result for 1 hour
    redisTemplate.opsForValue().set(cacheKey, objectMapper.writeValueAsString(result), 1, TimeUnit.HOURS);
    
    return result;
}

3. Data Model Design

Entity-Relationship Diagram

erDiagram
    WORD {
        string id PK
        string word
        integer frequency
        timestamp created_at
        timestamp updated_at
        boolean is_correct
    }
    
    SUGGESTION {
        string id PK
        string word_id FK
        string suggestion
        integer rank
        timestamp created_at
    }
    
    USER {
        string id PK
        string email
        string name
        timestamp created_at
    }
    
    USER_WORD_HISTORY {
        string id PK
        string user_id FK
        string word_id FK
        string action
        timestamp created_at
    }

    WORD ||--o{ SUGGESTION : "has"
    USER ||--o{ USER_WORD_HISTORY : "has"
    WORD ||--o{ USER_WORD_HISTORY : "has"

Table / Collection Schema Definitions

Words Table

CREATE TABLE words (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    word VARCHAR(255) NOT NULL UNIQUE,
    frequency INTEGER NOT NULL DEFAULT 0,
    is_correct BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_words_word ON words(word);
CREATE INDEX idx_words_frequency ON words(frequency DESC);
CREATE INDEX idx_words_is_correct ON words(is_correct);

Suggestions Table

CREATE TABLE suggestions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    word_id UUID NOT NULL REFERENCES words(id) ON DELETE CASCADE,
    suggestion VARCHAR(255) NOT NULL,
    rank INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    
    CONSTRAINT idx_suggestions_word_suggestion UNIQUE (word_id, suggestion)
);

CREATE INDEX idx_suggestions_word_id ON suggestions(word_id);
CREATE INDEX idx_suggestions_rank ON suggestions(rank DESC);

User Word History Table

CREATE TABLE user_word_history (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    word_id UUID NOT NULL REFERENCES words(id) ON DELETE CASCADE,
    action VARCHAR(50) NOT NULL, -- "corrected", "ignored", "misspelled"
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_user_word_history_user_id ON user_word_history(user_id);
CREATE INDEX idx_user_word_history_word_id ON user_word_history(word_id);
CREATE INDEX idx_user_word_history_created_at ON user_word_history(created_at);

Primary Key Strategy

UUID is selected as the primary key strategy for all tables because:

  • Provides globally unique identifiers without requiring coordination
  • Enables horizontal sharding based on UUID ranges or hash
  • Prevents enumeration attacks by making IDs unpredictable
  • Supports distributed system deployment without ID conflicts

Index Design

  1. Words Table:

    • idx_words_word: B-tree index on word field for fast lookups and exact matches
    • idx_words_frequency: B-tree index on frequency descending for quick access to most common words
    • idx_words_is_correct: B-tree index on is_correct for filtering valid words
  2. Suggestions Table:

    • idx_suggestions_word_id: B-tree index for foreign key lookups
    • idx_suggestions_rank: B-tree index on rank for efficient suggestion ordering
  3. User Word History Table:

    • idx_user_word_history_user_id: B-tree index for user-based queries
    • idx_user_word_history_word_id: B-tree index for word-based queries
    • idx_user_word_history_created_at: B-tree index for time-range queries

Data Partitioning / Sharding Strategy

Shard by word prefix (first 2 characters):

  • Hash the first two characters of the word to determine shard
  • Example: "hello" → shard 0x6865 (hex of "he")
  • This provides even distribution across shards
  • Allows for efficient prefix-based queries and suggestions

Soft Delete vs. Hard Delete Decision

Soft delete is chosen because:

  • Maintains historical data for user behavior analysis
  • Preserves suggestion quality tracking (what suggestions were made to users)
  • Enables data recovery in case of accidental deletions
  • Supports audit trails for compliance and debugging

Audit Trail Design

All data modifications are tracked through:

  • created_at timestamp for creation tracking
  • updated_at timestamp for modification tracking
  • User word history table for user-specific actions and corrections

Data Archival Strategy and Retention Policy

Active vs. Inactive Word Classification:

  • Words with frequency < 5 and updated_at < 90 days are considered inactive
  • Inactive words are archived to separate storage (e.g., S3) with metadata retention in DB
  • Archive retention: 1 year for all archived words and suggestions
-- Example archival trigger logic (pseudocode)
CREATE OR REPLACE FUNCTION archive_inactive_words()
RETURNS TRIGGER AS $$
BEGIN
    IF OLD.frequency < 5 AND OLD.updated_at < NOW() - INTERVAL '90 days' THEN
        -- Move to archive table or mark as archived
        INSERT INTO words_archive (id, word, frequency, is_correct, created_at, updated_at)
        VALUES (OLD.id, OLD.word, OLD.frequency, OLD.is_correct, OLD.created_at, OLD.updated_at);
        -- Keep in main table but mark as archived
        UPDATE words SET is_archived = TRUE WHERE id = OLD.id;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

4. Class Design & Service Layer

classDiagram
    class SpellCheckerService {
        +checkWord(word: String) : CheckResult
        +suggestCorrections(word: String) : List~String~
        +addWordToDictionary(word: String) : boolean
        +removeWordFromDictionary(word: String) : boolean
        +bulkAddWords(words: List~String~) : BulkOperationResult
    }

    class DictionaryRepository {
        +saveWord(word: String) : boolean
        +deleteWord(word: String) : boolean
        +exists(word: String) : boolean
        +findAllWords() : List~String~
        +findWordsByPrefix(prefix: String) : List~String~
    }

    class TrieNode {
        +children: Map~Character, TrieNode~
        +isEndOfWord: boolean
        +word: String
    }

    class Trie {
        +root: TrieNode
        +insert(word: String) : void
        +search(word: String) : boolean
        +startsWith(prefix: String) : List~String~
        +delete(word: String) : boolean
    }

    class EditDistanceCalculator {
        +calculateDistance(s1: String, s2: String) : int
        +isWithinEditDistance(s1: String, s2: String, maxDistance: int) : boolean
    }

    class CheckResult {
        +isValid: boolean
        +originalWord: String
        +suggestions: List~String~
    }

    class BulkOperationResult {
        +successful: int
        +failed: int
        +failures: List~String~
    }

    class SpellCheckerConfig {
        +maxEditDistance: int
        +dictionaryThreshold: int
        +suggestionLimit: int
    }

    SpellCheckerService --> DictionaryRepository : uses
    SpellCheckerService --> Trie : uses
    SpellCheckerService --> EditDistanceCalculator : uses
    DictionaryRepository --> Trie : uses
    Trie --> TrieNode : contains
    EditDistanceCalculator --> CheckResult : returns
stateDiagram-v2
    [*] --> Idle
    Idle --> Checking : checkWord()
    Checking --> Valid : word exists in dictionary
    Checking --> Invalid : word not found
    Invalid --> Suggesting : suggestCorrections()
    Suggesting --> Idle : suggestions returned
    Idle --> Updating : addWordToDictionary()
    Updating --> Idle : word added
    Idle --> Removing : removeWordFromDictionary()
    Removing --> Idle : word removed

Service Class Structure and Responsibilities

The spell checker system is built around the SpellCheckerService as the primary orchestrator, which delegates to specialized components:

  1. DictionaryRepository: Manages persistence of dictionary words
  2. Trie: Implements efficient word lookup and prefix matching
  3. EditDistanceCalculator: Computes string similarity for suggestions
  4. SpellCheckerConfig: Centralized configuration management

Interface / Abstract Class Design

public interface DictionaryRepository {
    boolean saveWord(String word);
    boolean deleteWord(String word);
    boolean exists(String word);
    List<String> findAllWords();
    List<String> findWordsByPrefix(String prefix);
}

public interface SpellCheckerConfig {
    int getMaxEditDistance();
    int getSuggestionLimit();
    int getDictionaryThreshold();
}

Business Logic Encapsulation Patterns

The system uses the Strategy Pattern for edit distance calculation to support different algorithms:

public interface EditDistanceStrategy {
    int calculate(String s1, String s2);
}

public class LevenshteinDistanceStrategy implements EditDistanceStrategy {
    @Override
    public int calculate(String s1, String s2) {
        // Implementation of Levenshtein distance algorithm
        int[][] dp = new int[s1.length() + 1][s2.length() + 1];
        for (int i = 0; i <= s1.length(); i++) dp[i][0] = i;
        for (int j = 0; j <= s2.length(); j++) dp[0][j] = j;
        
        for (int i = 1; i <= s1.length(); i++) {
            for (int j = 1; j <= s2.length(); j++) {
                if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = 1 + Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1]));
                }
            }
        }
        return dp[s1.length()][s2.length()];
    }
}

Critical-Path Method Implementation

@Transactional
public CheckResult checkWord(String word) {
    // Validate input
    if (word == null || word.trim().isEmpty()) {
        return new CheckResult(false, word, Collections.emptyList());
    }
    
    String normalizedWord = word.toLowerCase().trim();
    
    // Check if word exists in dictionary
    boolean isValid = dictionaryRepository.exists(normalizedWord);
    
    if (isValid) {
        return new CheckResult(true, normalizedWord, Collections.emptyList());
    }
    
    // If not valid, find suggestions
    List<String> suggestions = findSuggestions(normalizedWord);
    
    return new CheckResult(false, normalizedWord, suggestions);
}

private List<String> findSuggestions(String word) {
    // Get words with same prefix
    List<String> candidates = dictionaryRepository.findWordsByPrefix(word.substring(0, Math.min(word.length(), 3)));
    
    // Filter candidates by edit distance
    return candidates.stream()
        .filter(candidate -> {
            int distance = editDistanceCalculator.calculate(word, candidate);
            return distance <= config.getMaxEditDistance();
        })
        .sorted(Comparator.comparing(candidate -> {
            int distance = editDistanceCalculator.calculate(word, candidate);
            return distance;
        }))
        .limit(config.getSuggestionLimit())
        .collect(Collectors.toList());
}

Transaction Boundary Design

Single transaction boundaries are used for:

  • Word addition/removal operations (atomic persistence)
  • Bulk operations (all or nothing)
  • Suggestion generation (read-only, no transaction required)

Service Interface Contracts

public interface SpellCheckerService {
    CheckResult checkWord(String word);
    List<String> suggestCorrections(String word);
    boolean addWordToDictionary(String word);
    boolean removeWordFromDictionary(String word);
    BulkOperationResult bulkAddWords(List<String> words);
}

Saga / Orchestration Pattern

For bulk operations, a simple orchestration pattern is used where:

  1. Validate all words before processing
  2. Process each word in a batch operation
  3. Return aggregate result with success/failure details

Domain Object / Value Object Design

public class CheckResult {
    private final boolean isValid;
    private final String originalWord;
    private final List<String> suggestions;

    public CheckResult(boolean isValid, String originalWord, List<String> suggestions) {
        this.isValid = isValid;
        this.originalWord = originalWord;
        this.suggestions = suggestions != null ? new ArrayList<>(suggestions) : Collections.emptyList();
    }

    // Getters...
}

public class BulkOperationResult {
    private final int successful;
    private final int failed;
    private final List<String> failures;

    public BulkOperationResult(int successful, int failed, List<String> failures) {
        this.successful = successful;
        this.failed = failed;
        this.failures = failures != null ? new ArrayList<>(failures) : Collections.emptyList();
    }

    // Getters...
}

5. Concurrency & Thread Safety

Concurrency Model

The spell checker system will use a thread pool-based concurrency model with Java's ExecutorService for managing concurrent operations. The system will leverage the java.util.concurrent package extensively to ensure thread safety and optimal resource utilization.

Race Condition Prevention Strategy

Race conditions will be prevented through:

  1. Immutable data structures for dictionary and word frequency data
  2. Thread-local storage for temporary processing state
  3. Synchronized access to shared mutable state using read-write locks
  4. Atomic operations for counters and statistics

Optimistic vs. Pessimistic Locking Selection

Optimistic locking is selected for spell checker operations because:

  • The system primarily performs read-heavy operations (checking words, suggesting corrections)
  • Write operations (adding new words to dictionary) are infrequent and can be retried
  • The system handles concurrent spell checking requests from multiple users simultaneously
  • Optimistic locking provides better throughput for high-concurrency scenarios

Deadlock Prevention Strategy

Deadlock prevention will be implemented through:

  1. Lock ordering - all operations acquire locks in a consistent order (e.g., dictionary lock before word frequency lock)
  2. Timeout-based acquisition - all lock attempts have a 50ms timeout to prevent indefinite blocking
  3. Hierarchical lock management - locks are organized in a clear hierarchy to avoid circular dependencies
  4. Thread interruption handling - proper cleanup when threads are interrupted during lock acquisition

Idempotency Implementation for Concurrent Duplicate Requests

For concurrent duplicate spell checking requests, the system implements idempotency using:

public class SpellCheckerService {
    private final Map<String, CompletableFuture<SpellCheckResult>> concurrentRequests = new ConcurrentHashMap<>();
    
    public CompletableFuture<SpellCheckResult> checkSpellingAsync(String text) {
        String key = generateRequestKey(text);
        
        return concurrentRequests.computeIfAbsent(key, k -> {
            CompletableFuture<SpellCheckResult> future = CompletableFuture.supplyAsync(() -> {
                // Actual spell checking logic here
                return performSpellCheck(text);
            });
            
            future.whenComplete((result, error) -> {
                concurrentRequests.remove(key);
            });
            
            return future;
        });
    }
    
    private String generateRequestKey(String text) {
        return "spellcheck:" + DigestUtils.md5Hex(text);
    }
}

The system uses a hash-based key to identify duplicate requests and ensures that only one actual spell checking operation is performed for identical input, while concurrent callers receive the same result.

Thread Safety Patterns

The spell checker system employs several thread safety patterns:

  1. Immutable Dictionary Design: The dictionary is built once at startup and never modified, ensuring thread safety for read operations.

  2. Thread-Safe Statistics Collection: Using AtomicLong and ConcurrentHashMap for tracking statistics like word counts, correction suggestions, and error rates.

  3. Copy-on-Write Pattern: For dictionary updates, the system uses a copy-on-write approach where new versions are created and swapped atomically.

public class ThreadSafeSpellChecker {
    private volatile Map<String, Integer> wordFrequencies = new ConcurrentHashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public void updateWordFrequency(String word, int count) {
        lock.writeLock().lock();
        try {
            // Create new map with updated frequency
            Map<String, Integer> newMap = new HashMap<>(wordFrequencies);
            newMap.put(word, count);
            wordFrequencies = newMap;
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public int getWordFrequency(String word) {
        lock.readLock().lock();
        try {
            return wordFrequencies.getOrDefault(word, 0);
        } finally {
            lock.readLock().unlock();
        }
    }
}

Critical Path Concurrency Handling

The spell checker's critical path operation (spell checking) handles concurrency through:

  1. Read-Write Locks for dictionary access
  2. Thread-local storage for temporary suggestion collections
  3. Atomic counters for tracking concurrent operations
  4. Batch processing for multiple words in a single request

The system ensures that while multiple spell checking operations can run concurrently, dictionary reads and updates are properly synchronized to prevent race conditions.

Memory Consistency Model

The spell checker system leverages Java's memory consistency model:

  • volatile fields ensure visibility of dictionary updates across threads
  • Proper synchronization ensures that all threads see a consistent view of the spell checker state
  • Memory barriers are used where necessary to prevent reordering that could cause incorrect spell checking results

Performance Considerations

The concurrency model is designed to:

  • Handle up to 10,000 concurrent spell checking requests per second
  • Minimize lock contention through proper lock granularity
  • Use efficient thread pool sizing (typically 2x CPU cores for I/O-bound operations)
  • Prevent thread starvation through proper queue management and timeout handling

6. Sequence Diagrams for Key Flows

sequenceDiagram
    participant Client
    participant SpellCheckerService
    participant DictionaryRepository
    participant Cache
    participant SpellCheckResult

    Client->>SpellCheckerService: POST /spell-check
    SpellCheckerService->>Cache: Check if result cached
    Cache-->>SpellCheckerService: Cache miss
    SpellCheckerService->>DictionaryRepository: Fetch dictionary data
    DictionaryRepository-->>SpellCheckerService: Dictionary data
    SpellCheckerService->>SpellCheckerService: Perform spell check
    SpellCheckerService->>Cache: Store result in cache
    Cache-->>SpellCheckerService: Cache stored
    SpellCheckerService-->>Client: Spell check result
sequenceDiagram
    participant Client
    participant SpellCheckerService
    participant DictionaryRepository
    participant Cache
    participant Error

    Client->>SpellCheckerService: POST /spell-check
    SpellCheckerService->>Cache: Check if result cached
    Cache-->>SpellCheckerService: Cache hit
    SpellCheckerService-->>Client: Cached spell check result
sequenceDiagram
    participant SpellCheckerService
    participant DictionaryRepository
    participant EventPublisher
    participant SpellCheckEventConsumer

    SpellCheckerService->>DictionaryRepository: Fetch dictionary data
    DictionaryRepository-->>SpellCheckerService: Dictionary data
    SpellCheckerService->>EventPublisher: Publish spell check event
    EventPublisher->>SpellCheckEventConsumer: Deliver event
    SpellCheckEventConsumer-->>EventPublisher: Acknowledge receipt

7. Caching Design

Cache Strategy Selection

For spell checking, we'll use a cache-aside pattern with LRU eviction policy. This approach provides the best balance between performance and cache consistency for a spell checker where recent queries are most likely to be repeated.

Cache Key Design

Cache keys will follow a namespace strategy:

spellcheck:{user_id}:{query_hash}:{max_suggestions}

Where query_hash is a SHA-256 hash of the input query to prevent key length issues and ensure consistent key generation.

TTL Strategy

Data TypeTTLReason
Spell check results5 minutesRecent queries are likely to be repeated
Dictionary lookup1 hourDictionary data is relatively stable
User preferences24 hoursUser-specific settings change infrequently

Cache Eviction Policy

We'll implement LRU (Least Recently Used) eviction policy with a maximum cache size of 100,000 entries. This prevents memory bloat while maintaining high hit rates for frequently accessed spell check queries.

Cache Stampede Prevention

To prevent cache stampedes:

  1. Probabilistic early expiry: Set TTL to 50% of configured TTL (e.g., 2.5 minutes) for cache entries
  2. Distributed locks: Use Redis distributed locks (SET key value NX EX ttl) when cache misses occur to prevent multiple concurrent computations for the same query

Distributed Cache Architecture

We'll use Redis Cluster with:

  • 3 master nodes for data distribution
  • 3 replica nodes per master for high availability
  • Automatic failover with Redis Sentinel monitoring
  • Cross-zone replication for regional redundancy

Cache Invalidation Strategy

We'll use event-driven invalidation:

  1. When dictionary updates occur, invalidate all cache entries containing the modified words
  2. When user preferences change, invalidate user-specific cache entries
  3. TTL-based invalidation as a fallback for stale data
@Component
public class SpellCheckCache {
    private final RedisTemplate<String, SpellCheckResult> redisTemplate;
    private final ObjectMapper objectMapper;
    
    public SpellCheckResult getCachedResult(String userId, String query, int maxSuggestions) {
        String cacheKey = buildCacheKey(userId, query, maxSuggestions);
        
        // Try to get from cache
        SpellCheckResult cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return cached;
        }
        
        // Cache miss - use distributed lock to prevent stampede
        String lockKey = "lock:" + cacheKey;
        String lockValue = UUID.randomUUID().toString();
        
        try {
            if (redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 
                Duration.ofSeconds(10), TimeUnit.SECONDS)) {
                
                // Double-check cache after acquiring lock
                cached = redisTemplate.opsForValue().get(cacheKey);
                if (cached != null) {
                    return cached;
                }
                
                // Compute and cache result
                SpellCheckResult result = computeSpellCheck(query, maxSuggestions);
                redisTemplate.opsForValue().set(cacheKey, result, 
                    Duration.ofMinutes(5));
                
                return result;
            } else {
                // Another thread is computing, wait and retry
                Thread.sleep(50);
                return getCachedResult(userId, query, maxSuggestions);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Cache operation interrupted", e);
        } finally {
            // Release lock if acquired
            releaseLock(lockKey, lockValue);
        }
    }
    
    private String buildCacheKey(String userId, String query, int maxSuggestions) {
        String hash = DigestUtils.sha256Hex(query);
        return String.format("spellcheck:%s:%s:%d", userId, hash, maxSuggestions);
    }
    
    private void releaseLock(String lockKey, String lockValue) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), 
            Collections.singletonList(lockKey), lockValue);
    }
}

8. Repository / Data Access Layer

Repository Pattern Implementation

The spell checker system will implement the repository pattern to abstract data access logic and provide a clean interface for domain operations. Each core entity (Word, Dictionary, Suggestion) will have its own repository interface and implementation.

public interface WordRepository {
    Optional<Word> findByText(String text);
    List<Word> findByPrefix(String prefix, int limit);
    void save(Word word);
    void delete(String text);
    long count();
}

public interface DictionaryRepository {
    Optional<Dictionary> findById(String id);
    void save(Dictionary dictionary);
    List<Dictionary> findAll();
    void delete(String id);
}

public interface SuggestionRepository {
    void save(Suggestion suggestion);
    List<Suggestion> findByWordId(String wordId, int limit);
}

ORM vs Raw Query Decisions

For this system, we'll use JPA/Hibernate as the ORM for most operations due to:

  • Type safety and compile-time checking
  • Eager loading capabilities for performance
  • Built-in caching support through second-level cache
  • Simplified transaction management

However, for high-volume batch operations like dictionary building and bulk suggestion generation, raw SQL queries will be used to achieve better performance.

@Repository
public class WordRepositoryImpl implements WordRepository {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public Optional<Word> findByText(String text) {
        return entityManager.createQuery(
            "SELECT w FROM Word w WHERE w.text = :text", Word.class)
            .setParameter("text", text)
            .getResultStream()
            .findFirst();
    }
    
    @Override
    public List<Word> findByPrefix(String prefix, int limit) {
        return entityManager.createQuery(
            "SELECT w FROM Word w WHERE w.text LIKE :prefix ORDER BY w.frequency DESC", Word.class)
            .setParameter("prefix", prefix + "%")
            .setMaxResults(limit)
            .getResultList();
    }
    
    @Override
    public void save(Word word) {
        entityManager.merge(word);
    }
    
    @Override
    public void delete(String text) {
        entityManager.createQuery("DELETE FROM Word w WHERE w.text = :text")
            .setParameter("text", text)
            .executeUpdate();
    }
    
    @Override
    public long count() {
        return entityManager.createQuery("SELECT COUNT(w) FROM Word w", Long.class)
            .getSingleResult();
    }
}

Read vs Write Separation

For high-performance scenarios, we'll implement CQRS (Command Query Responsibility Segregation) pattern:

// Read model - optimized for queries
public class WordReadModel {
    private String text;
    private Long frequency;
    private String dictionaryId;
    
    // getters and setters
}

// Write model - optimized for mutations
public class WordWriteModel {
    private String text;
    private Long frequency;
    private String dictionaryId;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    // getters and setters
}

// Read-only repository for queries
public interface WordReadRepository {
    List<WordReadModel> findByPrefix(String prefix, int limit);
    WordReadModel findByText(String text);
}

// Write repository for mutations
public interface WordWriteRepository {
    void save(WordWriteModel word);
    void updateFrequency(String text, Long newFrequency);
}

Connection Pool Configuration

Connection pool settings optimized for spell checker workload:

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

N+1 Query Prevention Strategy

To prevent N+1 queries in suggestion generation:

// Using JOIN FETCH to eagerly load related entities
public List<Word> findWordsWithSuggestions(List<String> wordIds) {
    return entityManager.createQuery(
        "SELECT w FROM Word w JOIN FETCH w.suggestions WHERE w.id IN :wordIds", Word.class)
        .setParameter("wordIds", wordIds)
        .getResultList();
}

// Using batch loading for better performance
public Map<String, List<Suggestion>> getSuggestionsForWords(List<String> wordIds) {
    // Batch fetch all suggestions for given words in single query
    List<Suggestion> suggestions = entityManager.createQuery(
        "SELECT s FROM Suggestion s WHERE s.wordId IN :wordIds", Suggestion.class)
        .setParameter("wordIds", wordIds)
        .getResultList();
    
    return suggestions.stream()
        .collect(Collectors.groupingBy(Suggestion::getWordId));
}

Batch Operation Design

For bulk dictionary loading and word updates:

@Repository
public class BatchWordRepository {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void batchSave(List<Word> words) {
        int batchSize = 1000;
        
        for (int i = 0; i < words.size(); i += batchSize) {
            int end = Math.min(i + batchSize, words.size());
            List<Word> batch = words.subList(i, end);
            
            // Use bulk insert for better performance
            String sql = "INSERT INTO words (text, frequency, dictionary_id) VALUES ";
            // ... build parameterized query
            
            entityManager.createNativeQuery(sql).executeUpdate();
        }
    }
    
    public void batchUpdateFrequency(List<Word> words) {
        // Use bulk update operation to minimize round trips
        String sql = "UPDATE words SET frequency = CASE text ";
        // ... build conditional update query
        
        entityManager.createNativeQuery(sql).executeUpdate();
    }
}

Transaction Management

All repository operations will be wrapped in appropriate transaction boundaries:

@Service
@Transactional
public class SpellCheckerService {
    
    @Autowired
    private WordRepository wordRepository;
    
    @Autowired
    private DictionaryRepository dictionaryRepository;
    
    public void addWordToDictionary(String wordText, String dictionaryId) {
        Word word = new Word(wordText, 1L, dictionaryId);
        wordRepository.save(word);
        
        // This operation is atomic and will rollback if any exception occurs
    }
}

Data Access Layer Design Summary

  • ORM Usage: JPA/Hibernate for type safety and standard operations
  • CQRS Pattern: Separate read/write models for optimal performance
  • Connection Pooling: Configured for high throughput and low latency
  • N+1 Prevention: JOIN FETCH and batch operations to avoid performance bottlenecks
  • Batch Operations: Native queries for bulk data operations
  • Transaction Boundaries: Clear scope definition to ensure consistency and atomicity

This design provides a robust foundation for the spell checker system with proper separation of concerns, performance optimization, and scalability considerations.

9. Asynchronous Processing & Messaging

Event-driven boundaries in this feature

The spell checker system will process text input asynchronously to provide real-time suggestions and corrections. The key asynchronous boundaries are:

  1. Text Processing: When a user submits text for spell checking, the system will publish an event to trigger background processing.
  2. Suggestion Generation: After initial text analysis, suggestions are generated asynchronously using a separate worker pool.
  3. Result Caching: Once corrections are computed, they are cached for future use to reduce redundant processing.
  4. User Notification: When corrections are ready, a notification is sent to the user's client via WebSocket or push notifications.

The decision to make suggestion generation asynchronous is based on the fact that spell checking can be computationally expensive, especially for large documents or complex text analysis. Making it asynchronous allows the system to respond immediately to user requests while performing heavy lifting in the background.

Message Schema Design

{
  "version": "1.0",
  "event_type": "spell_check_requested",
  "payload": {
    "request_id": "uuid",
    "user_id": "string",
    "text_content": "string",
    "language_code": "string",
    "timestamp": "iso8601_timestamp"
  },
  "metadata": {
    "source_service": "user-service",
    "correlation_id": "uuid"
  }
}

For suggestion generation:

{
  "version": "1.0",
  "event_type": "spell_check_results_ready",
  "payload": {
    "request_id": "uuid",
    "user_id": "string",
    "corrections": [
      {
        "original_word": "string",
        "suggested_corrections": ["string"],
        "position": {
          "start": "integer",
          "end": "integer"
        }
      }
    ],
    "timestamp": "iso8601_timestamp"
  },
  "metadata": {
    "source_service": "spell-check-worker",
    "correlation_id": "uuid"
  }
}

Versioning strategy uses semantic versioning with major version as the event type prefix, allowing for backward compatibility in consumers.

Dead Letter Queue Strategy

The system will implement a dead letter queue (DLQ) strategy to handle failed message processing:

  1. Retry Limit: Messages will be retried up to 3 times with exponential backoff (1s, 4s, 9s)
  2. DLQ Handling: Messages that fail after max retries will be moved to a DLQ topic
  3. Poison Message Detection: Messages that consistently fail (e.g., same payload causing repeated failures) will be marked as poisoned and require manual intervention
  4. Monitoring: DLQ messages will trigger alerts to on-call engineers

Delivery Semantics

The system uses at-least-once delivery for spell checking events because:

  • Duplicate processing of the same text is acceptable (will produce identical results)
  • User experience requires that corrections are eventually delivered
  • The system can safely handle duplicate processing through idempotency checks

Retry Policy

Retry policy for consumers:

  • Max Attempts: 3 retries
  • Backoff Strategy: Exponential backoff with jitter (1s, 4s, 9s)
  • Non-Retryable Errors:
    • Invalid input format (400 errors) - no retry
    • Language not supported (400 errors) - no retry
    • User not found (401/403 errors) - no retry

Outbox Pattern

The spell checker service will NOT use the outbox pattern for event publishing because:

  1. Simplicity: The system is designed to be lightweight and focused on core spell checking functionality
  2. Immediate Processing: Spell checking results are typically needed immediately and don't require the complexity of eventual consistency
  3. Eventual Consistency: The system can tolerate some delay in publishing events to the message broker for minor corrections
  4. Alternative Approach: Using a simple transactional outbox pattern with database-backed events would add unnecessary complexity for this use case

Async Flow Sequence Diagram

sequenceDiagram
    participant User
    participant SpellCheckerService
    participant MessageBroker
    participant SpellCheckWorker
    participant Cache

    User->>SpellCheckerService: Submit text for spell checking
    SpellCheckerService->>MessageBroker: Publish spell_check_requested event
    MessageBroker->>SpellCheckWorker: Deliver event to worker
    SpellCheckWorker->>SpellCheckWorker: Process text (tokenization, spell checking)
    SpellCheckWorker->>Cache: Store corrections in cache
    SpellCheckWorker->>MessageBroker: Publish spell_check_results_ready event
    MessageBroker->>SpellCheckerService: Deliver results event
    SpellCheckerService->>User: Return suggestions to user

10. Error Handling Design

Error Classification Hierarchy

Errors are categorized into three main types to guide handling and response:

  1. Domain Errors - Business logic violations (e.g., invalid word, unsupported language)
  2. Infrastructure Errors - System-level issues (e.g., database connection failure, cache timeout)
  3. Validation Errors - Input format issues (e.g., missing parameters, malformed JSON)
public class SpellCheckerError extends RuntimeException {
    private final ErrorType errorType;
    private final String errorCode;

    public SpellCheckerError(ErrorType errorType, String errorCode, String message) {
        super(message);
        this.errorType = errorType;
        this.errorCode = errorCode;
    }

    public enum ErrorType {
        DOMAIN, INFRASTRUCTURE, VALIDATION
    }
}

public class InvalidWordException extends SpellCheckerError {
    public InvalidWordException(String word) {
        super(ErrorType.DOMAIN, "INVALID_WORD", "Word '" + word + "' is invalid");
    }
}

public class DatabaseConnectionException extends SpellCheckerError {
    public DatabaseConnectionException(String message) {
        super(ErrorType.INFRASTRUCTURE, "DB_CONNECTION_ERROR", message);
    }
}

Error Propagation Strategy

Errors are propagated up the call stack to appropriate layers for handling:

  • Validation errors are caught at API boundary and returned as 400 responses
  • Infrastructure errors are logged and may trigger circuit breaker logic or retry strategies
  • Domain errors are converted to appropriate HTTP responses (e.g., 400 for invalid word, 422 for unsupported language)

Retry Strategy

For transient infrastructure errors (e.g., cache timeouts, database connection issues), exponential backoff with jitter is applied:

public class RetryableSpellCheckerService {
    private static final int MAX_RETRIES = 3;
    private static final long BASE_DELAY_MS = 100;
    private static final double JITTER_FACTOR = 0.2;

    public SpellCheckResult spellCheckWithRetry(String word, String language) {
        int attempts = 0;
        while (true) {
            try {
                return spellCheck(word, language);
            } catch (DatabaseConnectionException e) {
                if (attempts >= MAX_RETRIES) {
                    throw e;
                }
                long delay = calculateBackoffDelay(attempts);
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Interrupted during retry", ie);
                }
                attempts++;
            }
        }
    }

    private long calculateBackoffDelay(int attempt) {
        long delay = (long) (BASE_DELAY_MS * Math.pow(2, attempt));
        long jitter = (long) (delay * JITTER_FACTOR * (Math.random() - 0.5) * 2);
        return delay + jitter;
    }
}

Circuit Breaker Design

For external dependencies like database or cache, a circuit breaker pattern is implemented:

public class CircuitBreaker {
    private final int failureThreshold;
    private final long timeoutMs;
    private int failureCount = 0;
    private long lastFailureTime = 0;
    private CircuitState state = CircuitState.CLOSED;

    public enum CircuitState {
        CLOSED, OPEN, HALF_OPEN
    }

    public boolean canExecute() {
        if (state == CircuitState.CLOSED) return true;
        if (state == CircuitState.OPEN && System.currentTimeMillis() - lastFailureTime > timeoutMs) {
            state = CircuitState.HALF_OPEN;
            return true;
        }
        return false;
    }

    public void recordSuccess() {
        if (state == CircuitState.HALF_OPEN) {
            state = CircuitState.CLOSED;
        }
        failureCount = 0;
    }

    public void recordFailure() {
        if (state == CircuitState.CLOSED) {
            failureCount++;
            if (failureCount >= failureThreshold) {
                state = CircuitState.OPEN;
                lastFailureTime = System.currentTimeMillis();
            }
        } else if (state == CircuitState.HALF_OPEN) {
            state = CircuitState.OPEN;
            lastFailureTime = System.currentTimeMillis();
        }
    }
}

Fallback Behavior Design

When a dependency fails, fallbacks provide graceful degradation:

public class FallbackSpellCheckerService {
    private final SpellCheckerService primaryService;
    private final SpellCheckerService fallbackService;

    public SpellCheckResult spellCheckWithFallback(String word, String language) {
        try {
            return primaryService.spellCheck(word, language);
        } catch (DatabaseConnectionException e) {
            // Fallback to in-memory dictionary or default behavior
            return fallbackService.spellCheck(word, language);
        } catch (CacheTimeoutException e) {
            // Return cached result if available or default
            return fallbackService.spellCheck(word, language);
        }
    }
}

Error Logging Format

Structured logging includes trace context and error details:

public class SpellCheckerErrorLogger {
    private static final Logger logger = LoggerFactory.getLogger(SpellCheckerErrorLogger.class);

    public void logError(String operation, Throwable error, Map<String, Object> context) {
        Map<String, Object> logContext = new HashMap<>(context);
        logContext.put("operation", operation);
        logContext.put("error_type", error.getClass().getSimpleName());
        logContext.put("error_message", error.getMessage());
        logContext.put("timestamp", Instant.now().toString());

        if (error instanceof SpellCheckerError) {
            SpellCheckerError spellError = (SpellCheckerError) error;
            logContext.put("error_code", spellError.getErrorCode());
            logContext.put("error_type_category", spellError.getErrorType().name());
        }

        logger.error("Spell checker operation failed", 
            MarkerFactory.getMarker("SPELL_CHECKER_ERROR"), 
            new ObjectMapper().valueToTree(logContext));
    }
}

Alerting Thresholds

Critical errors trigger alerts:

Error TypeAlert ThresholdDescription
Database Connection Failures> 5 failures in 5 minutesTriggers immediate alert
Cache Timeout Errors> 10 errors in 10 minutesTriggers warning alert
Invalid Word Errors> 100 errors in 1 hourTriggers warning alert

Error Response Format

Standardized JSON error response:

{
  "error": {
    "code": "INVALID_WORD",
    "message": "Word 'qwe' is invalid",
    "details": {
      "word": "qwe",
      "timestamp": "2023-05-15T10:30:00Z"
    }
  }
}

11. Security Design

Input Validation and Sanitization Strategy

All API inputs undergo strict validation using Apache Commons Validator for format checking and OWASP Java HTML Sanitizer for HTML content sanitization. Custom validation rules are implemented using Bean Validation annotations (@NotNull, @Size, @Pattern) with regex patterns for spell checker-specific constraints like maximum word length (100 characters) and allowed character sets (alphanumeric + common punctuation).

public class SpellCheckRequest {
    @NotBlank
    @Size(max = 100)
    @Pattern(regexp = "^[a-zA-Z0-9\\s\\-\\'\\.]+$")
    private String text;
    
    @NotNull
    private Language language = Language.ENGLISH;
}

SQL Injection Prevention

All database queries use parameterized prepared statements exclusively. No dynamic query construction or string concatenation occurs. The spell checker uses MyBatis with named parameters and follows the principle of least privilege for database users.

Authentication Middleware Design

API requests are authenticated using JWT tokens with 15-minute expiration times. The authentication middleware validates token signature, checks expiration, and extracts user context for authorization decisions. Token refresh is handled via a separate endpoint with rate limiting.

Authorization Enforcement

Role-Based Access Control (RBAC) is implemented where:

  • SPELL_CHECKER_USER can perform spell checks
  • SPELL_CHECKER_ADMIN has full access including dictionary management
  • Authorization checks occur at service layer using @PreAuthorize annotations
@PreAuthorize("hasRole('SPELL_CHECKER_USER') or hasRole('SPELL_CHECKER_ADMIN')")
public SpellCheckResult spellCheck(@Valid SpellCheckRequest request) {
    // Implementation
}

Sensitive Data Encryption at Rest and in Transit

All spell checker data is encrypted at rest using AES-256 with keys managed by AWS KMS. Data in transit uses TLS 1.3 with HSTS header enforcement and certificate pinning for client connections.

PII Handling and Masking Strategy

No personally identifiable information is stored or processed by the spell checker. All text processing strips out any PII patterns using regex-based detection and masking before processing.

Secrets Management

API keys, database credentials, and encryption keys are managed through AWS Secrets Manager with automatic rotation every 90 days. Application code accesses secrets via secure environment variables or AWS SDK with caching.

Audit Logging Design

All spell check operations are audited with structured JSON logs containing:

  • timestamp: ISO8601 formatted
  • user_id: From JWT claims
  • request_id: Unique operation identifier
  • action: "SPELL_CHECK"
  • input_length: Character count of input text
  • suggestions_count: Number of suggestions returned

Audit logs are stored in CloudWatch with 30-day retention policy and sent to SIEM for compliance monitoring.

12. Testing Strategy

Unit Testing Scope and Coverage Targets

  • 90%+ code coverage for business logic classes (service layer, domain objects)
  • 100% coverage for validation logic and error handling paths
  • Target: 85%+ for repository layer methods (mocked database interactions)
  • Unit tests will be written using JUnit 5 and Mockito for mocking dependencies
  • Each test class will follow the AAA (Arrange, Act, Assert) pattern with clear test naming

Integration Testing Strategy

  • Database integration tests using Testcontainers with PostgreSQL 13+ to verify actual SQL execution
  • External API integration tests using WireMock for dependency mocking (e.g., language detection services)
  • Caching layer integration using embedded Redis or in-memory cache implementations
  • Event-driven integration tests using embedded Kafka or RabbitMQ containers for async flows
  • End-to-end test fixtures will be pre-loaded with realistic test data (common misspellings, valid words, edge cases)

Contract Testing

  • Pact Broker integration for API contract testing between spell-checker service and upstream services (e.g., user-service, language-service)
  • Consumer-driven contracts for external service integrations (e.g., language detection, word frequency services)
  • Versioned contract schemas with semantic versioning to support backward compatibility
  • Contract verification pipeline as part of CI/CD for each service change

Performance Testing Tool and Key Scenarios

  • k6 for load testing with realistic concurrent user scenarios (1000+ concurrent spell-check requests)
  • Key performance scenarios:
    • Batch spell checking (100 words per request)
    • High-frequency single-word checks (1000+ requests/sec)
    • Memory usage under sustained load (500 concurrent users)
    • Latency distribution targets: p50 < 50ms, p95 < 200ms, p99 < 500ms
  • JMeter for stress testing and long-running performance validation (24-hour soak tests)

Test Data Management Strategy

  • Factory pattern for generating test data (valid words, misspellings, word frequency distributions)
  • Fixtures stored in JSON/YAML files with pre-defined test cases (common English misspellings, domain-specific terms)
  • Seed data pipeline to populate test database with realistic word lists (dictionary, user-specific corrections)
  • Test data isolation using database transaction rollbacks or dedicated test schemas per test suite
  • Data generation strategies:
    • Synonym-based word generation for testing related-word suggestions
    • Frequency-weighted random word selection to simulate real usage patterns
    • Multi-language test data for international spell checking capability
// Example unit test structure for spell checker service
@Test
void shouldReturnCorrectSuggestionsForMisspelledWord() {
    // Arrange
    String misspelled = "teh";
    List<String> expectedSuggestions = Arrays.asList("the", "they", "then");
    
    // Mock repository to return word frequency data
    when(wordRepository.findSimilarWords("teh")).thenReturn(expectedSuggestions);
    
    // Act
    List<String> suggestions = spellCheckerService.getSuggestions(misspelled);
    
    // Assert
    assertThat(suggestions).containsExactlyInAnyOrderElementsOf(expectedSuggestions);
}

13. Observability Design

Structured Logging Schema

All logs follow a consistent JSON schema to enable structured analysis and filtering:

{
  "timestamp": "2023-10-27T14:30:00.000Z",
  "level": "INFO|WARN|ERROR",
  "service": "spell-checker-service",
  "trace_id": "123e4567-e89b-12d3-a456-426614174000",
  "span_id": "abcdef1234567890",
  "user_id": "user-123",
  "request_id": "req-456",
  "operation": "checkSpelling",
  "entity_id": "doc-789",
  "message": "Spell check completed successfully",
  "duration_ms": 45,
  "error_code": "SPELL_CHECK_FAILED",
  "error_details": {
    "exception_type": "SpellCheckException",
    "stack_trace": "...",
    "cause": "Invalid word list"
  },
  "pii_masked": true
}

PII fields (user_id, email, etc.) are masked using a consistent pattern: *** or truncated values in logs. All sensitive fields are excluded from log output when possible.

Distributed Tracing Instrumentation

Using OpenTelemetry for distributed tracing:

  • Root Span: spell_check_request - captures entire request lifecycle
  • Child Spans:
    • word_normalization - time to normalize input words
    • dictionary_lookup - lookup in dictionary or trie structure
    • suggestion_generation - suggestion algorithm execution
    • cache_hit_or_miss - cache interaction timing

Key attributes captured per span:

span.setAttribute("spell_check.operation", "checkSpelling");
span.setAttribute("spell_check.word_count", words.size());
span.setAttribute("spell_check.cache_hit", cacheHit);
span.setAttribute("spell_check.suggestion_count", suggestions.size());

Metrics Design

Service-level RED metrics:

Metric NameTypeLabelsDescription
spell_check_requests_totalCounteroperation, status, error_codeTotal spell check requests
spell_check_duration_secondsHistogramoperation, statusRequest processing time
spell_check_errors_totalCountererror_code, operationError counts by type
spell_check_cache_hits_totalCountercache_type, hit_or_missCache hit/miss statistics
spell_check_suggestions_generated_totalHistogramsuggestion_countDistribution of suggestions

Metrics are exported via Prometheus and visualized in Grafana dashboards.

Health Check Endpoint Design

@GetMapping("/health")
public ResponseEntity<HealthStatus> health() {
    return ResponseEntity.ok(new HealthStatus(
        "spell-checker-service",
        "healthy",
        Map.of("database", "connected", "cache", "healthy")
    ));
}

@GetMapping("/ready")
public ResponseEntity<ReadyStatus> ready() {
    return ResponseEntity.ok(new ReadyStatus(
        "spell-checker-service",
        "ready",
        Map.of("dictionary_loaded", true, "cache_ready", true)
    ));
}

Alerting Thresholds

MetricWarning LevelCritical LevelAlerting Channel
spell_check_errors_total{status="error"}> 10 errors/minute> 50 errors/minuteSlack/Email
spell_check_duration_seconds{operation="checkSpelling"}(p95)> 200ms> 500msPagerDuty
spell_check_cache_hits_total{cache_type="redis"}< 80% hit rate< 60% hit rateSlack
spell_check_requests_totalDrop > 50% in 5 minDrop > 80% in 5 minOn-call

Alerts are configured with appropriate escalation paths and on-call rotation. Critical alerts trigger immediate notifications to the on-call engineer and Slack channel.

Service Readiness Checks

The /ready endpoint ensures:

  • Dictionary files are loaded and parsed correctly
  • Cache is initialized and responsive
  • Database connection pool has available connections
  • External dependencies (if any) are reachable

Log Rotation and Retention

Logs are rotated daily with retention policy:

  • 30 days of detailed logs (structured JSON)
  • 7 days of error-level logs (compressed)
  • 1 day of debug-level logs (compressed)

14. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering alignment

  • Dictionary size vs. memory footprint trade-off: Should we load the entire dictionary into memory or use a disk-based solution? This decision impacts startup time, memory usage, and latency. The choice should align with our service's memory constraints and expected query volume.
  • Real-time vs. batch spell-checking: Should we support real-time spell checking for live text input or batch processing of documents? This affects the architecture's complexity, latency requirements, and whether we need streaming or async processing capabilities.
  • Language support strategy: Should we support multiple languages or focus on a single language? Multi-language support increases complexity in data model, caching strategy, and system resource allocation.

Known trade-offs and justifications

Trade-offOption 1Option 2Justification
Memory vs. PerformanceLoad entire dictionary in memoryLoad on-demand from diskMemory usage is higher but provides better performance for frequent lookups. For a spell checker, the dictionary size is manageable (10s of MBs) and frequent lookups are expected.
Accuracy vs. SpeedUse sophisticated algorithms (e.g., Levenshtein with phonetic matching)Use simple edit distance or hash-based lookupsComplex algorithms provide better accuracy but increase latency. For a spell checker, we can accept some accuracy loss for better performance to meet user expectations.
Consistency vs. AvailabilityEnforce strict consistency for all spell check operationsAllow eventual consistency for some operationsFor real-time spell checking, availability is more important than strict consistency. Users can tolerate slight delays in updates but not complete unavailability.
Single Language vs. Multi-languageSupport only EnglishSupport multiple languagesSupporting multiple languages increases complexity in data model, indexing, and caching. For a production system, starting with one language and adding others as needed is more practical.

Risks and mitigation strategies

  • High memory usage: Risk of excessive memory consumption due to large dictionary or caching. Mitigation: Implement memory monitoring, set hard limits on cache size, and use memory-efficient data structures like Tries.
  • High latency under load: Risk of slow response times during peak usage. Mitigation: Implement caching, use connection pooling, and deploy horizontally with load balancing.
  • Inaccurate spell checking: Risk of false positives/negatives in spell checking. Mitigation: Use multiple algorithms, maintain a feedback loop for accuracy improvements, and allow manual overrides.
  • Dictionary update complexity: Risk of inconsistent dictionary state during updates. Mitigation: Use atomic updates, implement versioning for dictionaries, and use read-replicas for concurrent access.
  • Caching invalidation: Risk of serving stale spell check results. Mitigation: Implement TTL-based cache invalidation, use event-driven invalidation for dictionary updates, and maintain cache consistency through proper cache key design.

Technical debt acknowledged

  • Limited algorithmic diversity: Current implementation focuses on basic edit distance and hash lookups. Future technical debt includes implementing more sophisticated algorithms like n-gram analysis, phonetic matching (Soundex), or neural network-based spell checkers.
  • Single-threaded processing: Spell checking operations are currently single-threaded. This becomes a bottleneck under high load and requires future horizontal scaling or async processing.
  • No user feedback integration: The system does not currently incorporate user corrections to improve spell checking accuracy. This creates a technical debt in the form of missed opportunities for continuous improvement and machine learning integration.
  • Limited language support: The current design assumes a single language. Future work will require significant refactoring to support multiple languages and their specific spelling rules.
  • No integration with external spell-checking services: The system does not integrate with external services like Hunspell or commercial APIs, which could provide better accuracy and reduce maintenance burden. This creates technical debt in terms of ongoing maintenance and accuracy improvements.

Was this article helpful?

Topics in this article

More from Backend LLD

  • Backend HLD

    API Rate Limiter Design and Implementation

    Comprehensive backend system design for rate limiting APIs with token bucket and sliding window algorithms, including distributed architecture considerations.

  • Backend LLD

    CQRS Framework Design and Implementation

    Comprehensive low-level design for a Command Query Responsibility Segregation (CQRS) framework with event sourcing, read/write model separation, and distributed consistency patterns.

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.