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
- 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?
- What consistency requirements exist? Should the spell checker return real-time corrections, or can it tolerate slight delays for better accuracy?
- 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?
- What is the read/write ratio? Is this primarily a lookup service, or does it involve frequent dictionary updates?
- What latency sensitivity is acceptable? Should corrections happen in <10ms, or is a few hundred milliseconds acceptable?
- What authentication model is expected? Will the spell checker be used internally or exposed via an API with OAuth2/JWT?
- 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
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /v1/spell-check | Spell check a single word or text | JWT |
| POST | /v1/spell-check/batch | Spell check multiple words or texts in batch | JWT |
| GET | /v1/suggestions/{word} | Get spelling suggestions for a word | JWT |
| GET | /v1/suggestions/batch | Get suggestions for multiple words in batch | JWT |
| POST | /v1/word/add | Add a word to custom dictionary | JWT |
| DELETE | /v1/word/remove/{word} | Remove a word from custom dictionary | JWT |
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
| Code | Description |
|---|---|
| 200 | Success |
| 400 | Bad Request (malformed input) |
| 401 | Unauthorized |
| 403 | Forbidden |
| 429 | Rate Limited |
| 500 | Internal 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 operationsspellcheck:write- for write operations (add/remove words)
Rate Limiting
Rate limiting will be implemented per endpoint:
POST /v1/spell-check: 1000 req/minPOST /v1/spell-check/batch: 500 req/minGET /v1/suggestions/{word}: 2000 req/minPOST /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
-
Words Table:
idx_words_word: B-tree index on word field for fast lookups and exact matchesidx_words_frequency: B-tree index on frequency descending for quick access to most common wordsidx_words_is_correct: B-tree index on is_correct for filtering valid words
-
Suggestions Table:
idx_suggestions_word_id: B-tree index for foreign key lookupsidx_suggestions_rank: B-tree index on rank for efficient suggestion ordering
-
User Word History Table:
idx_user_word_history_user_id: B-tree index for user-based queriesidx_user_word_history_word_id: B-tree index for word-based queriesidx_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_attimestamp for creation trackingupdated_attimestamp 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 < 5andupdated_at < 90 daysare 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:
- DictionaryRepository: Manages persistence of dictionary words
- Trie: Implements efficient word lookup and prefix matching
- EditDistanceCalculator: Computes string similarity for suggestions
- 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:
- Validate all words before processing
- Process each word in a batch operation
- 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:
- Immutable data structures for dictionary and word frequency data
- Thread-local storage for temporary processing state
- Synchronized access to shared mutable state using read-write locks
- 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:
- Lock ordering - all operations acquire locks in a consistent order (e.g., dictionary lock before word frequency lock)
- Timeout-based acquisition - all lock attempts have a 50ms timeout to prevent indefinite blocking
- Hierarchical lock management - locks are organized in a clear hierarchy to avoid circular dependencies
- 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:
-
Immutable Dictionary Design: The dictionary is built once at startup and never modified, ensuring thread safety for read operations.
-
Thread-Safe Statistics Collection: Using
AtomicLongandConcurrentHashMapfor tracking statistics like word counts, correction suggestions, and error rates. -
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:
- Read-Write Locks for dictionary access
- Thread-local storage for temporary suggestion collections
- Atomic counters for tracking concurrent operations
- 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:
volatilefields 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 Type | TTL | Reason |
|---|---|---|
| Spell check results | 5 minutes | Recent queries are likely to be repeated |
| Dictionary lookup | 1 hour | Dictionary data is relatively stable |
| User preferences | 24 hours | User-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:
- Probabilistic early expiry: Set TTL to 50% of configured TTL (e.g., 2.5 minutes) for cache entries
- 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:
- When dictionary updates occur, invalidate all cache entries containing the modified words
- When user preferences change, invalidate user-specific cache entries
- 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:
- Text Processing: When a user submits text for spell checking, the system will publish an event to trigger background processing.
- Suggestion Generation: After initial text analysis, suggestions are generated asynchronously using a separate worker pool.
- Result Caching: Once corrections are computed, they are cached for future use to reduce redundant processing.
- 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:
- Retry Limit: Messages will be retried up to 3 times with exponential backoff (1s, 4s, 9s)
- DLQ Handling: Messages that fail after max retries will be moved to a DLQ topic
- Poison Message Detection: Messages that consistently fail (e.g., same payload causing repeated failures) will be marked as poisoned and require manual intervention
- 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:
- Simplicity: The system is designed to be lightweight and focused on core spell checking functionality
- Immediate Processing: Spell checking results are typically needed immediately and don't require the complexity of eventual consistency
- Eventual Consistency: The system can tolerate some delay in publishing events to the message broker for minor corrections
- 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:
- Domain Errors - Business logic violations (e.g., invalid word, unsupported language)
- Infrastructure Errors - System-level issues (e.g., database connection failure, cache timeout)
- 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 Type | Alert Threshold | Description |
|---|---|---|
| Database Connection Failures | > 5 failures in 5 minutes | Triggers immediate alert |
| Cache Timeout Errors | > 10 errors in 10 minutes | Triggers warning alert |
| Invalid Word Errors | > 100 errors in 1 hour | Triggers 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_USERcan perform spell checksSPELL_CHECKER_ADMINhas full access including dictionary management- Authorization checks occur at service layer using
@PreAuthorizeannotations
@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 formatteduser_id: From JWT claimsrequest_id: Unique operation identifieraction: "SPELL_CHECK"input_length: Character count of input textsuggestions_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 wordsdictionary_lookup- lookup in dictionary or trie structuresuggestion_generation- suggestion algorithm executioncache_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 Name | Type | Labels | Description |
|---|---|---|---|
spell_check_requests_total | Counter | operation, status, error_code | Total spell check requests |
spell_check_duration_seconds | Histogram | operation, status | Request processing time |
spell_check_errors_total | Counter | error_code, operation | Error counts by type |
spell_check_cache_hits_total | Counter | cache_type, hit_or_miss | Cache hit/miss statistics |
spell_check_suggestions_generated_total | Histogram | suggestion_count | Distribution 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
| Metric | Warning Level | Critical Level | Alerting Channel |
|---|---|---|---|
spell_check_errors_total{status="error"} | > 10 errors/minute | > 50 errors/minute | Slack/Email |
spell_check_duration_seconds{operation="checkSpelling"}(p95) | > 200ms | > 500ms | PagerDuty |
spell_check_cache_hits_total{cache_type="redis"} | < 80% hit rate | < 60% hit rate | Slack |
spell_check_requests_total | Drop > 50% in 5 min | Drop > 80% in 5 min | On-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-off | Option 1 | Option 2 | Justification |
|---|---|---|---|
| Memory vs. Performance | Load entire dictionary in memory | Load on-demand from disk | Memory 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. Speed | Use sophisticated algorithms (e.g., Levenshtein with phonetic matching) | Use simple edit distance or hash-based lookups | Complex 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. Availability | Enforce strict consistency for all spell check operations | Allow eventual consistency for some operations | For 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-language | Support only English | Support multiple languages | Supporting 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.