Database Architecture & Theory
Go Database Ecosystem
Go provides excellent support for both SQL and NoSQL databases through its standard library and rich ecosystem. Understanding database patterns, connection pooling, and transaction management is crucial for building robust applications.
SQL Databases
Relational databases with ACID properties, structured schemas, and powerful query capabilities using SQL.
ACID Relations TransactionsNoSQL Databases
Document, key-value, graph, and column-family databases designed for scalability and flexible data models.
Scalable Flexible DistributedConnection Pooling
Efficient management of database connections to optimize performance and resource utilization in concurrent applications.
Pool PerformancePostgreSQL Connection
Connect to PostgreSQL using the standard database/sql package with pq driver.
package main import ( "database/sql" "fmt" "log" _ "github.com/lib/pq" ) func connectPostgres() (*sql.DB, error) { connStr := "user=username dbname=mydb sslmode=disable" db, err := sql.Open("postgres", connStr) if err != nil { return nil, err } // Verify connection if err := db.Ping(); err != nil { return nil, err } // Configure connection pool db.SetMaxOpenConns(25) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(5 * time.Minute) return db, nil }
CRUD Operations
Implement Create, Read, Update, and Delete operations with prepared statements.
type User struct { ID int Email string Name string CreatedAt time.Time } // Create a new user func createUser(db *sql.DB, user User) (int64, error) { query := ` INSERT INTO users (email, name, created_at) VALUES ($1, $2, $3) RETURNING id` var id int64 err := db.QueryRow(query, user.Email, user.Name, time.Now()).Scan(&id) return id, err } // Read user by ID func getUserByID(db *sql.DB, id int) (*User, error) { var user User query := "SELECT id, email, name, created_at FROM users WHERE id = $1" err := db.QueryRow(query, id).Scan( &user.ID, &user.Email, &user.Name, &user.CreatedAt, ) if err == sql.ErrNoRows { return nil, fmt.Errorf("user not found") } return &user, err } // Update user func updateUser(db *sql.DB, user User) error { query := "UPDATE users SET email = $1, name = $2 WHERE id = $3" result, err := db.Exec(query, user.Email, user.Name, user.ID) if err != nil { return err } rows, err := result.RowsAffected() if err != nil { return err } if rows == 0 { return fmt.Errorf("user not found") } return nil } // Delete user func deleteUser(db *sql.DB, id int) error { query := "DELETE FROM users WHERE id = $1" _, err := db.Exec(query, id) return err }
Transactions
Handle database transactions for atomic operations.
func transferFunds(db *sql.DB, fromID, toID int, amount float64) error { // Start transaction tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() // Rollback if not committed // Debit from source account _, err = tx.Exec(` UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1`, amount, fromID, ) if err != nil { return err } // Credit to destination account _, err = tx.Exec(` UPDATE accounts SET balance = balance + $1 WHERE id = $2`, amount, toID, ) if err != nil { return err } // Commit transaction return tx.Commit() }
GORM ORM Usage
Use GORM for object-relational mapping and simpler database operations.
import ( "gorm.io/driver/postgres" "gorm.io/gorm" ) type Product struct { gorm.Model Code string Price uint } func gormExample() { dsn := "host=localhost user=gorm password=gorm dbname=gorm" db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { panic("failed to connect database") } // Auto Migrate db.AutoMigrate(&Product{}) // Create db.Create(&Product{Code: "D42", Price: 100}) // Read var product Product db.First(&product, 1) // find by primary key db.First(&product, "code = ?", "D42") // Update db.Model(&product).Update("Price", 200) db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // Delete db.Delete(&product, 1) }
MongoDB Integration
Connect and work with MongoDB using the official Go driver.
import ( "context" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/bson" ) func mongoExample() { // Connect to MongoDB client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI("mongodb://localhost:27017")) if err != nil { panic(err) } defer client.Disconnect(context.TODO()) // Get collection collection := client.Database("testdb").Collection("users") // Insert document user := bson.D{ {"name", "John Doe"}, {"email", "john@example.com"}, {"age", 30}, } result, err := collection.InsertOne(context.TODO(), user) // Find documents cursor, err := collection.Find(context.TODO(), bson.D{{"age", 30}}) if err != nil { panic(err) } var results []bson.M if err = cursor.All(context.TODO(), &results); err != nil { panic(err) } // Update document filter := bson.D{{"name", "John Doe"}} update := bson.D{{"$set", bson.D{{"age", 31}}}} _, err = collection.UpdateOne(context.TODO(), filter, update) }
Redis Caching
Implement caching with Redis to improve performance.
import ( "github.com/go-redis/redis/v8" "context" "encoding/json" "time" ) func redisExample() { // Connect to Redis rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password DB: 0, // default DB }) ctx := context.Background() // Set key-value err := rdb.Set(ctx, "key", "value", 5*time.Minute).Err() if err != nil { panic(err) } // Get value val, err := rdb.Get(ctx, "key").Result() if err == redis.Nil { fmt.Println("key does not exist") } else if err != nil { panic(err) } // Cache struct user := User{ID: 1, Name: "Alice", Email: "alice@example.com"} json, _ := json.Marshal(user) rdb.Set(ctx, "user:1", json, 10*time.Minute) // Retrieve cached struct val, _ = rdb.Get(ctx, "user:1").Result() var cachedUser User json.Unmarshal([]byte(val), &cachedUser) }
Database Migrations
Manage database schema changes with migration tools.
// Using golang-migrate/migrate // Install: go get -u github.com/golang-migrate/migrate/v4 // migrations/001_create_users.up.sql CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); // migrations/001_create_users.down.sql DROP TABLE IF EXISTS users; // Run migrations in Go import ( "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" ) func runMigrations(dbURL string) error { m, err := migrate.New( "file://migrations", dbURL, ) if err != nil { return err } // Run all up migrations if err := m.Up(); err != nil && err != migrate.ErrNoChange { return err } return nil }
Advanced Database Patterns
Production-ready patterns for scalable and reliable database applications.
// Repository pattern with interface segregation type UserRepository interface { Create(ctx context.Context, user *User) error GetByID(ctx context.Context, id string) (*User, error) GetByEmail(ctx context.Context, email string) (*User, error) Update(ctx context.Context, user *User) error Delete(ctx context.Context, id string) error List(ctx context.Context, filter UserFilter) ([]*User, error) } type PostgresUserRepository struct { db *sql.DB stmts map[string]*sql.Stmt } func NewPostgresUserRepository(db *sql.DB) (*PostgresUserRepository, error) { repo := &PostgresUserRepository{ db: db, stmts: make(map[string]*sql.Stmt), } // Prepare statements once queries := map[string]string{ "create": `INSERT INTO users (id, email, name, created_at) VALUES ($1, $2, $3, $4)`, "getById": `SELECT id, email, name, created_at, updated_at FROM users WHERE id = $1`, "getByEmail": `SELECT id, email, name, created_at, updated_at FROM users WHERE email = $1`, "update": `UPDATE users SET email = $2, name = $3, updated_at = $4 WHERE id = $1`, "delete": `DELETE FROM users WHERE id = $1`, "list": `SELECT id, email, name, created_at, updated_at FROM users WHERE ($1::text IS NULL OR name ILIKE '%' || $1 || '%') ORDER BY created_at DESC LIMIT $2 OFFSET $3`, } for name, query := range queries { stmt, err := db.Prepare(query) if err != nil { return nil, fmt.Errorf("preparing %s statement: %w", name, err) } repo.stmts[name] = stmt } return repo, nil } func (r *PostgresUserRepository) Create(ctx context.Context, user *User) error { _, err := r.stmts["create"].ExecContext(ctx, user.ID, user.Email, user.Name, time.Now()) if err != nil { var pqErr *pq.Error if errors.As(err, &pqErr) && pqErr.Code == "23505" { return ErrUserAlreadyExists } return fmt.Errorf("creating user: %w", err) } return nil } // Database transaction management type TxManager struct { db *sql.DB } func (tm *TxManager) WithTransaction(ctx context.Context, fn func(*sql.Tx) error) error { tx, err := tm.db.BeginTx(ctx, &sql.TxOptions{ Isolation: sql.LevelReadCommitted, }) if err != nil { return fmt.Errorf("beginning transaction: %w", err) } defer func() { if p := recover(); p != nil { tx.Rollback() panic(p) // re-throw panic after Rollback } else if err != nil { tx.Rollback() } else { err = tx.Commit() } }() err = fn(tx) return err } // Advanced connection pool configuration func ConfigureConnectionPool(db *sql.DB, config DatabaseConfig) { // Maximum number of open connections db.SetMaxOpenConns(config.MaxOpenConnections) // Maximum number of idle connections db.SetMaxIdleConns(config.MaxIdleConnections) // Maximum lifetime of a connection db.SetConnMaxLifetime(time.Duration(config.ConnectionMaxLifetime) * time.Second) // Maximum idle time for a connection db.SetConnMaxIdleTime(time.Duration(config.ConnectionMaxIdleTime) * time.Second) } // Database health monitoring type HealthChecker struct { db *sql.DB } func (hc *HealthChecker) CheckHealth(ctx context.Context) error { // Check basic connectivity if err := hc.db.PingContext(ctx); err != nil { return fmt.Errorf("database ping failed: %w", err) } // Check connection pool stats stats := hc.db.Stats() if stats.OpenConnections >= stats.MaxOpenConnections { return errors.New("connection pool exhausted") } // Perform a simple query var result int if err := hc.db.QueryRowContext(ctx, "SELECT 1").Scan(&result); err != nil { return fmt.Errorf("health check query failed: %w", err) } return nil } // Query builder pattern type QueryBuilder struct { table string selects []string wheres []WhereClause orders []OrderClause limit *int offset *int args []interface{} } type WhereClause struct { Column string Operator string Value interface{} } func NewQueryBuilder(table string) *QueryBuilder { return &QueryBuilder{ table: table, args: make([]interface{}, 0), } } func (qb *QueryBuilder) Select(columns ...string) *QueryBuilder { qb.selects = append(qb.selects, columns...) return qb } func (qb *QueryBuilder) Where(column, operator string, value interface{}) *QueryBuilder { qb.wheres = append(qb.wheres, WhereClause{ Column: column, Operator: operator, Value: value, }) qb.args = append(qb.args, value) return qb } func (qb *QueryBuilder) OrderBy(column string, direction string) *QueryBuilder { qb.orders = append(qb.orders, OrderClause{ Column: column, Direction: direction, }) return qb } func (qb *QueryBuilder) Limit(limit int) *QueryBuilder { qb.limit = &limit return qb } func (qb *QueryBuilder) Build() (string, []interface{}) { var query strings.Builder // SELECT clause if len(qb.selects) > 0 { query.WriteString("SELECT ") query.WriteString(strings.Join(qb.selects, ", ")) } else { query.WriteString("SELECT *") } // FROM clause query.WriteString(" FROM ") query.WriteString(qb.table) // WHERE clause if len(qb.wheres) > 0 { query.WriteString(" WHERE ") for i, where := range qb.wheres { if i > 0 { query.WriteString(" AND ") } query.WriteString(fmt.Sprintf("%s %s $%d", where.Column, where.Operator, i+1)) } } // ORDER BY clause if len(qb.orders) > 0 { query.WriteString(" ORDER BY ") orderStrs := make([]string, len(qb.orders)) for i, order := range qb.orders { orderStrs[i] = fmt.Sprintf("%s %s", order.Column, order.Direction) } query.WriteString(strings.Join(orderStrs, ", ")) } // LIMIT clause if qb.limit != nil { query.WriteString(fmt.Sprintf(" LIMIT %d", *qb.limit)) } // OFFSET clause if qb.offset != nil { query.WriteString(fmt.Sprintf(" OFFSET %d", *qb.offset)) } return query.String(), qb.args }
Database Technology Comparison
Choosing the Right Database
Different databases excel in different scenarios. Understanding their strengths and trade-offs is crucial for architectural decisions.
PostgreSQL
Best for: Complex queries, ACID compliance
- Full ACID compliance
- Advanced SQL features
- JSON/JSONB support
- Extensible with plugins
- Strong consistency
MySQL
Best for: Web applications, read-heavy workloads
- High performance reads
- Master-slave replication
- Wide ecosystem support
- Storage engine options
- Easy to scale horizontally
MongoDB
Best for: Document storage, flexible schemas
- Document-oriented
- Horizontal scaling
- Flexible schema
- Built-in sharding
- Rich aggregation pipeline
Redis
Best for: Caching, session storage, real-time analytics
- In-memory performance
- Rich data structures
- Pub/Sub messaging
- Lua scripting support
- Persistence options
Database | Type | ACID | Scalability | Performance | Best Use Case |
---|---|---|---|---|---|
PostgreSQL | Relational | Full | Vertical | High | Complex applications, analytics |
MySQL | Relational | Full | Horizontal | Very High | Web applications, CMS |
MongoDB | Document | Limited | Horizontal | High | Content management, IoT |
Redis | Key-Value | No | Horizontal | Ultra High | Caching, real-time features |
Elasticsearch | Search | No | Horizontal | High | Search, log analysis |
Production Best Practices
Connection Management
- Configure appropriate pool sizes
- Set connection lifetimes
- Monitor connection usage
- Implement health checks
- Handle connection failures gracefully
Query Optimization
- Use prepared statements
- Add appropriate indexes
- Implement query timeouts
- Monitor slow queries
- Use connection context cancellation
Security & Reliability
- Prevent SQL injection attacks
- Use transactions for atomic operations
- Implement proper error handling
- Use database migrations
- Backup and recovery strategies
Database Performance Checklist
- Connection pooling: Optimize MaxOpenConns, MaxIdleConns, ConnMaxLifetime
- Prepared statements: Cache and reuse for better performance
- Context timeouts: Prevent long-running queries from blocking
- Transaction boundaries: Keep transactions short and focused
- Index optimization: Create indexes for frequently queried columns
- Query monitoring: Log and analyze slow queries regularly
Common Database Anti-Patterns
- N+1 queries: Loading related data in loops instead of joins
- Unbounded queries: SELECT without LIMIT on large tables
- Long transactions: Holding locks for extended periods
- Connection leaks: Not properly closing database connections
- SQL injection vulnerabilities: Not using parameterized queries
Database Challenges
Hands-On Database Projects
Master database operations with these practical challenges:
1. Multi-Tenant SaaS Database
Design and implement a multi-tenant database architecture with proper data isolation and efficient querying.
Beginner Multi-tenant2. Event Sourcing System
Build an event-sourced application with PostgreSQL, implementing event store, snapshots, and projections.
Intermediate Events3. Real-time Analytics Pipeline
Create a high-throughput data pipeline combining PostgreSQL, Redis, and Elasticsearch for real-time analytics.
Advanced Real-time4. Distributed Database System
Implement a distributed database with sharding, replication, and consistency guarantees across multiple nodes.
Expert Distributed