Audits 7 ADRs marked "Proposed" against the actual code, updates the Status field of 5 that are at least partially implemented. Keeps 2 as "Proposed" because only test infrastructure exists (no production implementation). Updated: - 0018 user-management-auth-system : Partially Implemented (auth/jwt/repos exist; auth middleware + greet integration missing) - 0019 postgresql-integration : Partially Implemented (postgres repo exists, BDD uses it; sqlite still present, not default) - 0022 rate-limiting-cache-strategy : Implemented (Phase 1) - Phase 2 still Proposed (PRs #22 ratelimit, #23 cache; Redis/Dragonfly deferred) - 0024 bdd-test-organization-and-isolation : Partially Implemented (domain dirs + scenario state isolation; parallel exec opt-in only) - 0025 bdd-scenario-isolation-strategies : Partially Implemented (schema-per-scenario opt-in via BDD_SCHEMA_ISOLATION; cache/user store isolation missing) Kept "Proposed" (production code not implemented, only test fixtures): - 0021 jwt-secret-retention-policy (BDD scenarios exist but no ConfigManager / cleanup goroutine in pkg/) - 0023 config-hot-reloading (testserver has reload, but no Viper WatchConfig in production) Audit method: Q-024 compliant - every status decision has file:line evidence documented in workspaces/adr-audit-status/stages/01-audit/output/audit-report.md. Generated ~95% in autonomy by Mistral Vibe via ICM workspace ~/Work/Vibe/workspaces/adr-audit-status/. Cost €2.50 stage 01-audit (very thorough). Trainer (Claude) finalized commit/PR (Mistral hit max-price). 🤖 Co-Authored-By: Mistral Vibe (devstral-2 / mistral-medium-3.5) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
23 KiB
19. PostgreSQL Database Integration
Date: 2026-04-07 Status: Partially Implemented Authors: Product Owner Decision Drivers: Data Persistence, Scalability, Production Readiness
Context
The dance-lessons-coach application currently uses SQLite with GORM for the user management system (ADR 0018), but since there are no existing users or production data, we can implement PostgreSQL directly as our primary database without migration concerns.
Current State
- Database: SQLite (in-memory mode) - no persistent data
- ORM: GORM v1.31.1
- Implementation:
pkg/user/sqlite_repository.go - Usage: User management system only
- Data: No existing users or production data
Implementation Drivers
- Production Readiness: PostgreSQL is enterprise-grade and production-ready
- Data Persistence: Proper persistent storage for user accounts
- Concurrency: PostgreSQL handles concurrent connections better
- Scalability: PostgreSQL supports horizontal scaling
- Features: Advanced PostgreSQL features (JSONB, full-text search)
- Ecosystem: Better tooling and monitoring for PostgreSQL
Decision
We will implement PostgreSQL database directly, replacing the SQLite implementation with the following characteristics:
Core Features
-
Database Setup
- PostgreSQL 15+ for production compatibility
- Containerized development environment
- Connection pooling for performance
- SSL support for secure connections
-
ORM Integration
- GORM as the primary ORM
- Interface-based repository pattern
- Database migrations for schema management
- Transaction support for data integrity
-
Configuration Management
- Viper integration for database settings
- Environment variable support with DLC_ prefix
- Multiple environment support (dev, staging, prod)
- Connection health checking
-
Integration Points
- User management system (ADR 0018)
- Existing greet service (for future personalization)
- OpenTelemetry tracing integration
- Zerolog structured logging
Technical Implementation
Database Schema Foundation
-- Users table (from ADR 0018)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
description TEXT,
current_goal TEXT,
is_admin BOOLEAN DEFAULT FALSE,
allow_password_reset BOOLEAN DEFAULT FALSE,
last_login TIMESTAMP WITH TIME ZONE
);
-- Greet history table (future extension)
CREATE TABLE greet_history (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER REFERENCES users(id),
message TEXT NOT NULL,
context JSONB
);
Technology Stack
- Database: PostgreSQL 15+ - production-ready relational database
- ORM: GORM v1.25+ - aligns with interface-based design
- Migrations: GORM AutoMigrate + custom SQL migrations
- Connection Pooling: PgBouncer-compatible connection management
- Configuration: Viper integration - consistent with existing patterns
- Logging: Zerolog integration - structured database logging
- Telemetry: OpenTelemetry database instrumentation
Architecture Alignment
The PostgreSQL integration follows established dance-lessons-coach patterns:
-
Interface-based Design:
type DatabaseRepository interface { GetDB() *gorm.DB Close() error HealthCheck(ctx context.Context) error BeginTransaction(ctx context.Context) (*gorm.DB, error) } type UserRepository interface { CreateUser(ctx context.Context, user *User) error GetUserByUsername(ctx context.Context, username string) (*User, error) // ... other methods } -
Context-aware Services:
func (r *PostgresUserRepository) CreateUser(ctx context.Context, user *User) error { log.Trace().Ctx(ctx).Str("username", user.Username).Msg("Creating user") return r.db.WithContext(ctx).Create(user).Error } -
Configuration Integration:
type DatabaseConfig struct { Type string `mapstructure:"type"` // sqlite, postgres, auto Host string `mapstructure:"host"` Port int `mapstructure:"port"` User string `mapstructure:"user"` Password string `mapstructure:"password"` Name string `mapstructure:"name"` SSLMode string `mapstructure:"ssl_mode"` MaxOpenConns int `mapstructure:"max_open_conns"` MaxIdleConns int `mapstructure:"max_idle_conns"` ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` } -
Graceful Shutdown Integration:
func (s *Server) Shutdown(ctx context.Context) error { // Close database connections gracefully if s.userRepo != nil { if err := s.userRepo.Close(); err != nil { log.Error().Err(err).Msg("User repository shutdown failed") // Continue shutdown even if database fails } } // The readiness endpoint already handles shutdown detection via s.readyCtx // No need for atomic operations - the context-based approach is cleaner // Continue with existing HTTP server shutdown return s.httpServer.Shutdown(ctx) } -
Readiness Endpoint Integration:
func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) { // Check database health if using persistent database if s.config.GetDatabaseType() != "sqlite" { if err := s.userRepo.CheckDatabaseHealth(r.Context()); err != nil { log.Warn().Err(err).Msg("Database health check failed") s.writeJSONResponse(w, http.StatusServiceUnavailable, map[string]interface{}{ "ready": false, "reason": "database_unhealthy", "error": err.Error(), }) return } } // Existing readiness logic select { case <-s.readyCtx.Done(): s.writeJSONResponse(w, http.StatusServiceUnavailable, map[string]interface{}{ "ready": false, "reason": "shutting_down", }) default: s.writeJSONResponse(w, http.StatusOK, map[string]interface{}{ "ready": true, }) } }
Implementation Strategy
Phase 1: PostgreSQL Repository Implementation
-
Replace Dependencies:
# Remove SQLite dependencies go get gorm.io/driver/postgres go get github.com/lib/pq # PostgreSQL driver go mod tidy # Clean up unused dependencies -
Create PostgreSQL Repository:
pkg/user/postgres_repository.go- PostgreSQL implementation- Implement
UserRepositoryinterface directly - Add PostgreSQL-specific connection management
-
Docker Setup:
- Create
docker-compose.ymlwith PostgreSQL 16 service (current stable version) - Add initialization scripts for development
- Configure health checks and monitoring
- Use Alpine-based image for smaller footprint
- Create
-
Configuration:
- Add
DatabaseConfigto existing config structure - Environment variables with
DLC_prefix - Connection validation and health checking
- Add
Phase 2: Server Integration
-
Update Server Initialization:
- Modify
initializeUserServices()inpkg/server/server.go - Replace SQLite repository with PostgreSQL repository
- Update error handling and logging
- Modify
-
Remove SQLite Code:
- Delete
pkg/user/sqlite_repository.go - Clean up any SQLite-specific references
- Update imports and dependencies
- Delete
-
Enhance Health Checks:
- Add database health check to readiness endpoint
- Implement connection pooling monitoring
- Add startup health validation
Phase 3: Testing & Validation
-
BDD Test Integration:
- Updated test server configuration with PostgreSQL settings
- Automatic PostgreSQL container startup in test script
- Health checks for database readiness before tests
- Separate BDD test database (
dance_lessons_coach_bdd_test) - Complete isolation from development/production databases
-
Test Script Enhancement:
scripts/run-bdd-tests.shnow starts PostgreSQL if needed- Automatic BDD database creation using
createdbcommand - Checks for existing BDD database before creating
- Waits for database readiness before running tests
- Proper error handling and timeout management
- Reuses existing container if already running
-
Database Isolation Strategy:
- Development:
dance_lessons_coach(config.yaml) - BDD Tests:
dance_lessons_coach_bdd_test(automatically created) - Production: Custom name per environment
- Manual Testing: Developers can use development database
- Development:
-
Unit & Integration Tests:
- Repository method testing with PostgreSQL
- Transaction and error case testing
- Performance benchmarks
- Connection failure scenarios
-
Graceful Shutdown Testing:
- Database connection cleanup during shutdown
- Readiness endpoint behavior during shutdown
- Connection pool behavior under stress
Phase 4: Documentation & Finalization
-
Documentation Updates:
- Update AGENTS.md with PostgreSQL setup instructions
- Add database configuration guide
- Create development setup documentation
- Update BDD test documentation
-
Cleanup:
- Remove all SQLite references from code
- Update go.mod and go.sum
- Verify no unused imports or dependencies
-
Production Readiness:
- Add database health monitoring
- Configure connection pooling for production
- Add environment-specific configurations
-
User Model & Repository:
pkg/user/models.go- GORM user modelpkg/user/repository.go- GORM implementationpkg/user/repository_mock.go- Mock for testing
-
Database Integration:
- Implement
UserRepositoryinterface - Add transaction support
- Implement health checks
- Implement
-
Testing Setup:
- Test container for PostgreSQL
- Integration test suite
- Mock-based unit tests
Phase 3: Service Integration
-
Auth Service Integration:
- Update auth service to use user repository
- Implement JWT token persistence
- Add session management
-
Greet Service Extension:
- Add greet history tracking
- Implement user-specific greetings
- Add database logging
-
API Endpoints:
- Health check endpoint:
GET /api/health/db - Database metrics endpoint:
GET /api/metrics/db
- Health check endpoint:
Phase 4: Testing & Validation
-
BDD Test Integration:
- Temporary test database setup
- Test container for PostgreSQL
- Clean database between scenarios
- Test data isolation
-
Unit & Integration Tests:
- Repository method testing
- Transaction testing
- Error case testing
- Performance benchmarks
-
Fallback Testing:
- SQLite fallback scenarios
- Connection failure handling
- Graceful degradation
Consequences
Positive
- Data Persistence: User accounts and application data properly persisted
- Production Ready: PostgreSQL is enterprise-grade database
- Scalability: Better concurrent connection handling
- Simplified Architecture: Direct PostgreSQL implementation without migration complexity
- Clean Codebase: No legacy SQLite code or dual implementation
- Future-Proof: Foundation for all future data-driven features
Negative
- Dependency Changes: Replacing SQLite with PostgreSQL dependencies
- Operational Overhead: Database container management
- Learning Curve: PostgreSQL-specific features and optimization
- Testing Requirements: Comprehensive testing needed for new implementation
Neutral
- Code Changes: Repository implementation replacement
- Configuration Updates: New database configuration structure
- Development Workflow: Docker-based database for local development
Alternatives Considered
Alternative 1: Keep SQLite with File Persistence
- Pros: Simple, no new dependencies, works for small-scale
- Cons: Not production-grade, limited concurrency, file-based limitations
- Rejected: Doesn't meet long-term production requirements
Alternative 2: Dual Implementation with Fallback
- Pros: Smooth migration path, backward compatibility
- Cons: Complex codebase, testing overhead, maintenance burden
- Rejected: Unnecessary complexity since no existing data or users
Alternative 2: MySQL
- Pros: Widely used, good community support
- Cons: Different ecosystem, licensing concerns
- Rejected: PostgreSQL better fits our needs
Alternative 3: MongoDB
- Pros: Flexible schema, document-oriented
- Cons: NoSQL approach, different query patterns
- Rejected: Relational data better suits our model
Alternative 4: Pure SQL (no ORM)
- Pros: No ORM overhead, direct control
- Cons: More boilerplate, manual query building
- Rejected: GORM provides good balance
Graceful Shutdown & Readiness Integration
Database Connection Lifecycle
The PostgreSQL integration must properly handle the server lifecycle:
-
Startup Sequence:
- Initialize database connections
- Run health check
- Set readiness to true only if database is healthy
- Log connection details at trace level
-
Runtime Operation:
- Monitor database connection health
- Handle connection failures gracefully
- Implement connection retry logic
- Log connection issues appropriately
-
Shutdown Sequence:
- Set readiness to false immediately
- Close all database connections
- Wait for in-flight queries to complete
- Handle shutdown timeouts gracefully
- Log shutdown progress
Readiness Endpoint Enhancement
The existing /api/ready endpoint already has the correct nested structure for service health checks. We'll enhance it to include PostgreSQL database health:
Current Structure:
{
"ready": true,
"connections": {
"database": {
"status": "healthy"
}
}
}
Health Check Logic:
func (r *PostgresUserRepository) CheckDatabaseHealth(ctx context.Context) error {
// Simple query to test connectivity
var count int64
result := r.db.WithContext(ctx).Model(&User{}).Count(&count)
if result.Error != nil {
return fmt.Errorf("database health check failed: %w", result.Error)
}
return nil
}
Readiness Response States:
- Healthy:
{"ready": true, "connections": {"database": {"status": "healthy"}}} - Database Unhealthy:
{"ready": false, "reason": "database_unhealthy", "connections": {"database": {"status": "unhealthy", "error": "connection refused"}}} - Shutting Down:
{"ready": false, "reason": "server_shutting_down", "connections": {"database": "not_checked"}} - Not Configured:
{"ready": true, "connections": {"database": {"status": "not_configured"}}}(for SQLite mode)
Connection Pool Management
Proper connection pool configuration for graceful shutdown:
// In database initialization
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get SQL DB: %w", err)
}
// Configure connection pool
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
// Configure graceful connection handling
sqlDB.SetConnMaxIdleTime(time.Minute * 5)
sqlDB.SetConnMaxLifetime(time.Hour * 1)
Shutdown Timeout Handling
func (s *Server) Shutdown(ctx context.Context) error {
// Create shutdown context with timeout
shutdownCtx, cancel := context.WithTimeout(ctx, s.config.GetShutdownTimeout())
defer cancel()
// Close database connections with timeout
done := make(chan struct{})
go func() {
if s.userRepo != nil {
if err := s.userRepo.Close(); err != nil {
log.Error().Err(err).Msg("Database shutdown error")
}
}
close(done)
}()
select {
case <-done:
log.Trace().Msg("Database shutdown completed")
case <-shutdownCtx.Done():
log.Warn().Msg("Database shutdown timed out, forcing closure")
}
return s.httpServer.Shutdown(shutdownCtx)
}
Alignment with Existing Architecture
This implementation builds upon completed phases:
- Phase 1-3: Uses Go 1.26.1, Chi router, Zerolog, interface-based design
- Phase 5: Extends Viper configuration management
- Phase 6: Integrates with graceful shutdown patterns and readiness endpoints
- Phase 7: Maintains OpenTelemetry compatibility
- Phase 8: Follows existing build system patterns
- Phase 9: Preserves trace-level logging approach
- Phase 18: Supports user management system
Backward Compatibility
The implementation maintains full backward compatibility:
- API Endpoints: Existing endpoints unchanged
- Configuration: All existing config options preserved
- Logging: Maintains existing Zerolog integration
- Telemetry: OpenTelemetry continues to work
- Error Handling: Consistent error patterns
Success Metrics
- Reliability: 99.9% database uptime
- Performance: <100ms average query time
- Scalability: Support 1000+ concurrent connections
- Data Integrity: Zero data corruption incidents
- Adoption: All new features use database storage
Open Questions
- What should be the connection pool size for production?
- Should we implement read replicas for scaling?
- What backup strategy should we implement?
- Should we add database connection health metrics?
- What query timeout should we set for production?
Database Cleanup Strategy
Decision: Raw SQL Cleanup Between Scenarios
Approach: Use raw SQL DELETE statements with SET CONSTRAINTS ALL DEFERRED to clean up database between test scenarios
Rationale:
- Black Box Principle: BDD tests should not depend on implementation details
- Foreign Key Safety:
SET CONSTRAINTS ALL DEFERREDallows proper handling of constraints (PostgreSQL docs: https://www.postgresql.org/docs/current/sql-set-constraints.html) - Migration Compatibility: Works regardless of schema changes
- Transaction Safety: Uses explicit transactions with proper rollback handling
Alternatives Considered:
- Repository-based cleanup - Rejected: Violates black box principle
- Transaction rollback - Rejected: Complex with nested transactions
- Recreate database - Rejected: Too slow for frequent test runs
- Separate test database - Chosen: Combined with SQL cleanup
Implementation Details
Cleanup Process:
- Disable constraints temporarily:
SET CONSTRAINTS ALL DEFERRED - Query all tables: From
information_schema.tables - Delete in reverse order: Handle foreign key dependencies
- Reset sequences:
ALTER SEQUENCE ... RESTART WITH 1
Execution Timing:
- AfterSuite: Full cleanup after all scenarios
- Between Scenarios: Individual scenario cleanup (future enhancement)
Benefits:
- ✅ Fast execution: Milliseconds vs seconds for recreation
- ✅ Reliable: Handles schema changes automatically
- ✅ Isolated: Each test gets clean state
- ✅ Maintainable: No dependency on ORM or repositories
Temporary Database Approach
For BDD testing, we'll use temporary PostgreSQL databases to ensure:
- Isolation: Each test run gets a clean database
- Reproducibility: Consistent starting state
- Performance: No interference between tests
- CI/CD Compatibility: Works in containerized environments
Implementation Plan
-
Test Container Setup:
# Use testcontainers-go for PostgreSQL go get github.com/testcontainers/testcontainers-go go get github.com/testcontainers/testcontainers-go/modules/postgres -
BDD Test Configuration:
- Create
features/support/database.go - Implement
BeforeScenarioandAfterScenariohooks - Automatic database cleanup
- Integrate with existing test suite structure
- Create
-
Test Data Management:
- Schema migration before each scenario
- Transaction rollback for data isolation
- Seed data for specific scenarios
- Match existing BDD test patterns
-
Configuration:
# config.test.yaml database: host: "localhost" port: 5433 # Different from dev port name: "dance_lessons_coach_test" user: "test_user" password: "test_password"
Example Test Setup
// features/support/database.go
func BeforeScenario(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
// Start PostgreSQL container
postgresContainer, err := postgres.RunContainer(ctx,
testcontainers.WithImage("postgres:15-alpine"),
postgres.WithDatabase("test_db"),
postgres.WithUsername("test_user"),
postgres.WithPassword("test_password"),
)
if err != nil {
return ctx, err
}
// Get connection string
connStr, err := postgresContainer.ConnectionString(ctx, "sslmode=disable")
if err != nil {
return ctx, err
}
// Store in context for test
ctx = context.WithValue(ctx, "postgres_container", postgresContainer)
ctx = context.WithValue(ctx, "postgres_conn_str", connStr)
// Initialize user repository with test database
config := config.GetTestConfig()
config.Database.DSN = connStr
repo, err := user.NewPostgresRepository(config)
if err != nil {
return ctx, err
}
// Store repository in context for scenario steps
ctx = context.WithValue(ctx, "user_repository", repo)
return ctx, nil
}
func AfterScenario(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) {
// Clean up repository
if repo, ok := ctx.Value("user_repository").(user.UserRepository); ok {
repo.Close()
}
// Terminate PostgreSQL container
if container, ok := ctx.Value("postgres_container").(testcontainers.Container); ok {
if terminateErr := container.Terminate(ctx); terminateErr != nil {
log.Error().Err(terminateErr).Msg("Failed to terminate PostgreSQL container")
}
}
return ctx, err
}
Future Considerations
Immediate Next Steps (Post-Migration)
- CI/CD Integration: Add PostgreSQL to CI pipeline
- Performance Tuning: Query optimization
- Monitoring: Database health metrics
- Backup Strategy: Regular database backups
Long-Term Enhancements
- Database Sharding: For horizontal scaling
- Read Replicas: For read-heavy workloads
- Advanced Caching: Redis integration
- Database Monitoring: Prometheus exporter
- Backup Automation: Regular backup scheduling
- Query Optimization: Performance tuning
References
- GORM Documentation
- PostgreSQL 16 Documentation
- PostgreSQL Latest Version
- GORM + PostgreSQL Guide
- Database Connection Pooling
Approved by: [Product Owner] Approval Date: [To be determined] Implementation Target: Q2 2024