Storage Adapters Overview¶
Storage adapters are the backbone of PromptManager, providing the interface between your prompts and their persistence layer. They allow you to store prompts in various backends while maintaining a consistent API.
What are Storage Adapters?¶
Storage adapters implement a common interface that handles:
- Loading prompt text and parameters
- Saving changes back to storage
- Listing available prompts
- Searching through prompt collections
- Managing prompt metadata
Think of them as the "database drivers" for your prompts - they handle the specifics of where and how your prompts are stored.
Adapter Architecture¶
graph TB
subgraph "Your Application"
APP[Application Code]
PROMPT[PromptManager::Prompt]
end
subgraph "Adapter Layer"
INTERFACE[Storage Adapter Interface]
FS[FileSystemAdapter]
AR[ActiveRecordAdapter]
CUSTOM[Custom Adapters]
end
subgraph "Storage Backends"
FILES[File System]
POSTGRES[PostgreSQL]
MYSQL[MySQL]
REDIS[Redis]
S3[AWS S3]
end
APP --> PROMPT
PROMPT --> INTERFACE
INTERFACE --> FS
INTERFACE --> AR
INTERFACE --> CUSTOM
FS --> FILES
AR --> POSTGRES
AR --> MYSQL
CUSTOM --> REDIS
CUSTOM --> S3
Available Adapters¶
PromptManager includes two built-in adapters:
FileSystemAdapter¶
- Best for: Development, small teams, file-based workflows
- Storage: Local files (
.txt
for prompts,.json
for parameters) - Features: Simple setup, version control friendly, human-readable
- Use cases: Personal projects, documentation, prototype development
ActiveRecordAdapter¶
- Best for: Web applications, enterprise deployments, shared access
- Storage: Any database supported by ActiveRecord (PostgreSQL, MySQL, SQLite, etc.)
- Features: Transactions, concurrent access, advanced querying
- Use cases: Production applications, multi-user systems, API backends
Common Interface¶
All storage adapters implement the same core methods:
module StorageAdapterInterface
# Load a prompt's text and parameters
def get(id)
# Returns: [text_string, parameters_hash]
end
# Save a prompt's text and parameters
def save(id, text, parameters)
# Persists changes to storage
end
# Delete a prompt
def delete(id)
# Removes prompt from storage
end
# List all available prompt IDs
def list
# Returns: array of prompt ID strings
end
# Search for prompts (optional)
def search(query)
# Returns: array of matching prompt IDs
end
end
Choosing an Adapter¶
Use FileSystemAdapter When:¶
✅ Starting a new project - Simple setup and configuration
✅ Working solo or small team - Easy to manage and understand
✅ Version control integration - Files work well with Git
✅ Human-readable storage - Can edit prompts directly
✅ Prototyping - Quick to set up and iterate
❌ Avoid when: High concurrency, complex querying, or web deployment
Use ActiveRecordAdapter When:¶
✅ Web applications - Integrates with existing Rails/web apps
✅ Multi-user systems - Handles concurrent access properly
✅ Complex queries - SQL-based searching and filtering
✅ Production deployment - Scalable and reliable
✅ Advanced features - Transactions, migrations, backups
❌ Avoid when: Simple scripts, file-based workflows, or development experimentation
Configuration Patterns¶
Global Configuration¶
Set the adapter once for your entire application:
# At application startup
PromptManager::Prompt.storage_adapter =
PromptManager::Storage::FileSystemAdapter.config do |config|
config.prompts_dir = Rails.root.join('prompts')
end.new
Per-Instance Configuration¶
Use different adapters for different prompt types:
# Customer service prompts from filesystem
customer_service = PromptManager::Prompt.new(id: 'greeting')
customer_service.storage_adapter = filesystem_adapter
# System prompts from database
system_prompt = PromptManager::Prompt.new(id: 'api_template')
system_prompt.storage_adapter = activerecord_adapter
Environment-Based Configuration¶
Different adapters for different environments:
adapter = case Rails.env
when 'development'
PromptManager::Storage::FileSystemAdapter.config do |config|
config.prompts_dir = 'prompts'
end.new
when 'production'
PromptManager::Storage::ActiveRecordAdapter.config do |config|
config.model = PromptModel
end.new
end
PromptManager::Prompt.storage_adapter = adapter
Adapter Comparison¶
Feature | FileSystemAdapter | ActiveRecordAdapter | Custom Adapters |
---|---|---|---|
Setup Complexity | Low | Medium | Varies |
Concurrent Access | Limited | Excellent | Depends |
Search Performance | Good with tools | Excellent | Varies |
Version Control | Excellent | Poor | Varies |
Backup/Recovery | File-based | Database tools | Depends |
Human Readable | Yes | No | Depends |
Scalability | Limited | High | Varies |
Query Complexity | Limited | High | Varies |
Migration Between Adapters¶
You can migrate prompts between different adapters:
# Migration helper
class AdapterMigration
def self.migrate(from_adapter, to_adapter)
prompt_ids = from_adapter.list
prompt_ids.each do |id|
text, parameters = from_adapter.get(id)
to_adapter.save(id, text, parameters)
puts "Migrated: #{id}"
end
end
end
# Usage
old_adapter = FileSystemAdapter.new(prompts_dir: 'old_prompts')
new_adapter = ActiveRecordAdapter.new(model: PromptModel)
AdapterMigration.migrate(old_adapter, new_adapter)
Performance Considerations¶
FileSystemAdapter Performance¶
Strengths:
- Fast individual prompt access
- No database overhead
- Works with external search tools (ripgrep, grep)
Limitations: - Slow listing operations with many files - No built-in indexing - Limited concurrent write support
Optimization tips:
# Use custom search for better performance
adapter.config.search_proc = ->(query) {
`rg -l "#{query}" #{prompts_dir}`.split("\n").map { |f|
File.basename(f, '.txt')
}
}
ActiveRecordAdapter Performance¶
Strengths: - SQL-based querying and indexing - Optimized for concurrent access - Built-in caching support
Limitations:
- Database connection overhead
- More complex deployment
Optimization tips:
# Add database indexes
class CreatePrompts < ActiveRecord::Migration[7.0]
def change
create_table :prompts do |t|
t.string :name, null: false
t.text :content
t.json :parameters
t.timestamps
end
add_index :prompts, :name, unique: true
add_index :prompts, :created_at
# Add full-text search index for content
add_index :prompts, :content, using: :gin if adapter_name == 'PostgreSQL'
end
end
Error Handling¶
All adapters should handle common error scenarios:
begin
prompt = PromptManager::Prompt.new(id: 'example')
text = prompt.to_s
rescue PromptManager::StorageError => e
case e.message
when /not found/
puts "Prompt doesn't exist: #{e.id}"
when /permission denied/
puts "Cannot access storage: #{e.message}"
when /connection failed/
puts "Storage backend unavailable: #{e.message}"
else
puts "Storage error: #{e.message}"
end
end
Testing with Adapters¶
Test Adapter¶
For testing, create a simple in-memory adapter:
class TestAdapter
def initialize
@storage = {}
end
def get(id)
@storage[id] || raise("Prompt not found: #{id}")
end
def save(id, text, parameters)
@storage[id] = [text, parameters]
end
def list
@storage.keys
end
end
# In your tests
RSpec.describe "PromptManager" do
let(:adapter) { TestAdapter.new }
before do
PromptManager::Prompt.storage_adapter = adapter
adapter.save('test', 'Hello [NAME]', { '[NAME]' => 'World' })
end
it "processes prompts correctly" do
prompt = PromptManager::Prompt.new(id: 'test')
expect(prompt.to_s).to eq('Hello World')
end
end
Adapter Testing¶
Test your custom adapters with a shared test suite:
# Shared examples for adapter testing
RSpec.shared_examples "storage adapter" do |adapter_factory|
let(:adapter) { adapter_factory.call }
it "saves and retrieves prompts" do
adapter.save('test', 'Hello [NAME]', { '[NAME]' => 'World' })
text, params = adapter.get('test')
expect(text).to eq('Hello [NAME]')
expect(params).to eq({ '[NAME]' => 'World' })
end
it "lists available prompts" do
adapter.save('test1', 'Content 1', {})
adapter.save('test2', 'Content 2', {})
expect(adapter.list).to contain_exactly('test1', 'test2')
end
it "raises error for missing prompts" do
expect { adapter.get('missing') }.to raise_error(PromptManager::StorageError)
end
end
# Test your adapters
RSpec.describe FileSystemAdapter do
include_examples "storage adapter", -> {
FileSystemAdapter.new(prompts_dir: temp_dir)
}
end
Best Practices¶
1. Configuration Management¶
# Use environment variables for sensitive config
database_config = {
host: ENV.fetch('DATABASE_HOST', 'localhost'),
password: ENV['DATABASE_PASSWORD'],
pool: ENV.fetch('DATABASE_POOL', 5).to_i
}
2. Error Recovery¶
class ResilientAdapter
def initialize(primary, fallback)
@primary = primary
@fallback = fallback
end
def get(id)
@primary.get(id)
rescue => e
Rails.logger.warn "Primary adapter failed: #{e.message}"
@fallback.get(id)
end
end
3. Caching Layer¶
class CachingAdapter
def initialize(adapter, cache_store = Rails.cache)
@adapter = adapter
@cache = cache_store
end
def get(id)
@cache.fetch("prompt:#{id}", expires_in: 1.hour) do
@adapter.get(id)
end
end
end
4. Monitoring and Logging¶
class InstrumentedAdapter
def initialize(adapter)
@adapter = adapter
end
def get(id)
start_time = Time.current
result = @adapter.get(id)
duration = Time.current - start_time
Rails.logger.info "Prompt loaded: #{id} (#{duration}s)"
result
end
end
Next Steps¶
- Learn specific adapters: FileSystemAdapter | ActiveRecordAdapter
- Build custom adapters: Custom Adapters Guide
- See examples: Real-world Usage