← Back to Cecep Azhar
Cecep Azhar #database#saas#postgresql#multi-tenant#design-patterns

Database Design Patterns untuk Aplikasi SaaS Modern

Cecep Saeful Azhar • 16 Oct 2026
Database Design Patterns untuk Aplikasi SaaS Modern
Pelajari database design patterns untuk aplikasi SaaS modern. Multi-tenancy, partitioning, caching, dan optimization dari pengalaman production.

Database untuk SaaS Bukan Sekadar CRUD

Mendesain database untuk SaaS application memiliki tantangan yang berbeda dari aplikasi biasa. Multi-tenancy, data isolation, scalability, dan performance di bawah concurrent load adalah aspek yang harus dipertimbangkan. Dari beberapa proyek SaaS yang saya bangun di Bandung, berikut patterns yang paling efektif.

PostgreSQL adalah pilihan utama saya untuk SaaS karena fitur-fiturnya yang powerful seperti JSONB, partitioning, dan row-level security.

Multi-Tenancy Patterns

Ada tiga pendekatan utama untuk multi-tenancy. Yang paling sering saya gunakan adalah shared database dengan tenant_id:

-- Schema multi-tenant dengan tenant_id
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(tenant_id, email)
);

-- Index untuk performance
CREATE INDEX idx_users_tenant ON users(tenant_id);
CREATE INDEX idx_users_email ON users(email);

-- Row-Level Security untuk data isolation
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON users
    USING (tenant_id = current_setting('app.current_tenant')::UUID);

Row-Level Security PostgreSQL memastikan setiap query hanya mengembalikan data milik tenant yang sedang aktif. Ini memberikan isolation yang kuat di level database.

Partitioning untuk Skalabilitas

Untuk tabel yang sangat besar (logs, analytics, time-series data), partitioning adalah kunci performa:

-- Partitioning berdasarkan waktu
CREATE TABLE events (
    id BIGSERIAL,
    tenant_id UUID NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    payload JSONB,
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);

-- Buat partitions per bulan
CREATE TABLE events_2024_01 PARTITION OF events
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE events_2024_02 PARTITION OF events
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- Query otomatis hanya scan partition yang relevan
SELECT * FROM events 
WHERE tenant_id = 'xxx' 
AND created_at >= '2024-01-01' AND created_at < '2024-02-01';

Connection Pooling dengan PgBouncer

Di production, saya selalu gunakan PgBouncer sebagai connection pooler. PostgreSQL memiliki overhead yang cukup besar untuk setiap connection baru, dan PgBouncer mengatasi masalah ini dengan efektif. Saya biasanya set max connections di PgBouncer sekitar 50-100, dan biarkan PostgreSQL melayani lebih sedikit connection langsung.

Caching Strategy

Untuk data yang sering di-read tapi jarang di-update, saya implement caching dengan Redis:

type CachedRepository struct {
    db    *sql.DB
    cache *redis.Client
    ttl   time.Duration
}

func (r *CachedRepository) GetUser(ctx context.Context, id string) (*User, error) {
    // Try cache first
    cacheKey := fmt.Sprintf("user:%s", id)
    cached, err := r.cache.Get(ctx, cacheKey).Result()
    if err == nil {
        var user User
        json.Unmarshal([]byte(cached), &user)
        return &user, nil
    }
    
    // Cache miss - query database
    var user User
    err = r.db.QueryRowContext(ctx,
        "SELECT id, email, name FROM users WHERE id = $1", id,
    ).Scan(&user.ID, &user.Email, &user.Name)
    if err != nil {
        return nil, err
    }
    
    // Store in cache
    userData, _ := json.Marshal(user)
    r.cache.Set(ctx, cacheKey, userData, r.ttl)
    
    return &user, nil
}

Best Practices Lainnya

Beberapa hal penting lainnya: selalu gunakan migrations untuk schema changes, implement soft delete untuk data critical, gunakan optimistic locking untuk concurrent updates, dan jangan lupa untuk定期 analyze dan vacuum tables untuk menjaga performa tetap optimal.

← More articles from Cecep Azhar Central Content Hub Home →