Databases & Storage
Choosing the right database is one of the highest-leverage decisions in system design. The wrong choice creates friction for years; the right choice makes scaling feel natural. This file covers the major database families, their trade-offs, and the operational techniques that keep them running at scale.
Relational Databases
Relational databases store data in tables with rows and columns. Relationships are expressed through foreign keys and enforced through constraints. SQL is the query language.
Strengths
- ACID transactions guarantee correctness for complex operations
- Joins let you query across tables efficiently
- Mature tooling, broad community knowledge
- Schema enforcement catches data quality issues early
When to Use
- Data has clear structure and relationships (users, orders, inventory)
- You need multi-row transactions (transfer money between accounts)
- Complex queries with joins, aggregations, and subqueries
- Correctness is more important than raw write throughput
Examples
- PostgreSQL — feature-rich, extensible, strong community
- MySQL — widely deployed, good replication ecosystem
- Amazon Aurora — managed, MySQL/PostgreSQL-compatible, auto-scaling storage
NoSQL Databases
NoSQL is a broad category covering any database that does not use the traditional relational model. The main families are:
Key-Value Stores
Data is a simple mapping from key to value. Fastest possible reads and writes when access is by primary key.
- Use for: session storage, shopping carts, feature flags
- Examples: Redis, DynamoDB, Memcached
Document Stores
Data is stored as JSON-like documents. Each document can have a different structure. Queries can reach into nested fields.
- Use for: content management, user profiles, catalogs with varied attributes
- Examples: MongoDB, Couchbase, Firestore
Column-Family Stores
Data is organized by column families rather than rows. Optimized for write-heavy workloads and time-series data with wide rows.
- Use for: event logging, IoT telemetry, analytics
- Examples: Cassandra, HBase, ScyllaDB
Graph Databases
Data is modeled as nodes and edges. Queries traverse relationships, which is far more natural and efficient than multi-table joins for graph-shaped data.
- Use for: social networks, recommendation engines, fraud detection
- Examples: Neo4j, Amazon Neptune, JanusGraph
ACID vs BASE
ACID
Atomicity, Consistency, Isolation, Durability. The gold standard for correctness.
- Atomicity: A transaction either fully commits or fully rolls back.
- Consistency: Data moves from one valid state to another.
- Isolation: Concurrent transactions don't interfere with each other.
- Durability: Committed data survives crashes.
Relational databases provide ACID. Some NoSQL databases offer it per-partition (DynamoDB transactions, MongoDB multi-document transactions).
BASE
Basically Available, Soft state, Eventually consistent. A pragmatic relaxation of ACID.
- The system is always available for reads and writes.
- State may be stale for a window of time.
- Given enough time with no new writes, all replicas converge.
Distributed NoSQL systems often default to BASE because strong consistency across nodes is expensive in terms of latency and availability (see the CAP theorem).
Choosing Between Them
If a stale read causes financial loss or data corruption, you need ACID. If a user seeing a 2-second-old follower count is acceptable, BASE is fine and lets you scale further.
Indexing
An index is a data structure that speeds up reads at the cost of slower writes and additional storage.
B-Tree Indexes
The default index type in most relational databases. Good for equality lookups, range queries, and sorting.
CREATE INDEX idx_users_email ON users(email);
Query: SELECT * FROM users WHERE email = 'alice@example.com';
Without index: full table scan (O(n))
With index: B-tree lookup (O(log n))
Hash Indexes
Exact-match lookups only. O(1) average case. No range queries.
Composite Indexes
Index on multiple columns. Column order matters — the index is useful only for queries that filter on a left prefix of the columns.
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
Useful for: WHERE user_id = 42 AND created_at > '2025-01-01'
Useful for: WHERE user_id = 42
NOT useful for: WHERE created_at > '2025-01-01' (skips leading column)
Full-Text Indexes
Inverted indexes that map words to the documents containing them. Essential for search features. PostgreSQL has built-in full-text search; for heavy workloads, use Elasticsearch or Solr.
Over-Indexing
Every index slows down writes (the index must be updated) and uses disk space. Index only the columns that appear in frequent queries. Monitor slow query logs to find missing indexes.
Replication
Replication copies data across multiple database nodes for availability and read scalability.
Single-Leader Replication
One primary node accepts all writes. Replicas asynchronously (or synchronously) receive a copy of every write.
Client writes -> Primary -> Replication log -> Replica 1
-> Replica 2
Client reads -> Replica 1 or Replica 2
- Simple model, easy to reason about
- Writes are limited to one node's capacity
- Async replication means replicas can lag (replication lag)
Multi-Leader Replication
Multiple nodes accept writes. Used in multi-region setups so each region has a local writer.
- Lower write latency for geographically distributed users
- Conflict resolution is hard (last-write-wins, custom merge logic)
Leaderless Replication
Any node accepts reads and writes. Uses quorum-based consistency (read from R nodes, write to W nodes, where R + W > N).
- High availability — no single point of failure for writes
- Requires careful quorum configuration
- Examples: Cassandra, DynamoDB
Sharding
Sharding splits data across multiple database instances (shards) so that no single instance holds everything.
When to Shard
Shard when a single database instance cannot handle the write throughput, storage volume, or query load. Sharding adds significant operational complexity, so exhaust simpler options first (indexing, read replicas, caching, vertical scaling).
Shard Key Selection
The shard key determines which shard holds a given row. A good shard key:
- Distributes data evenly across shards
- Aligns with the most common query patterns (queries that filter by shard key hit one shard)
- Avoids hot spots (a celebrity user_id receiving all traffic)
Sharding Strategies
- Hash-based: Hash the shard key, mod by number of shards. Even distribution but range queries span all shards.
- Range-based: Assign key ranges to shards (A-M on shard 1, N-Z on shard 2). Supports range queries but risks uneven distribution.
- Geographic: Shard by region. US data on US shard, EU data on EU shard. Good for data residency compliance.
Cross-Shard Queries
Queries that span shards are expensive. They require a scatter-gather pattern: query all relevant shards, merge results. Design your shard key to minimize these.
Choosing the Right Database
Decision Framework
Structured data + complex queries + transactions?
-> Relational (PostgreSQL, MySQL)
Simple key-based access at massive scale?
-> Key-value (DynamoDB, Redis)
Flexible schema + document-oriented access?
-> Document store (MongoDB)
Write-heavy + time-series + wide rows?
-> Column-family (Cassandra)
Highly connected data + traversal queries?
-> Graph (Neo4j)
Full-text search + ranking?
-> Search engine (Elasticsearch)
Real-World: Uber
Uber migrated from PostgreSQL to a custom system built on MySQL and Schemaless (a document-like layer on top of MySQL shards). The primary access pattern was key-based lookup of trip data, which didn't need PostgreSQL's advanced features but did need extreme write throughput and horizontal sharding.
Real-World: Discord
Discord stores billions of messages in Cassandra, chosen for its write throughput and ability to handle wide partitions (all messages in a channel). Metadata like guilds and users lives in PostgreSQL, where relational integrity matters more.
Common Pitfalls
- Premature sharding. Sharding adds operational cost (backups, migrations, cross-shard queries). Try read replicas, caching, and better indexing first.
- Choosing NoSQL because it's trendy. Most applications are well-served by a relational database. Reach for NoSQL when you have a specific access pattern or scale requirement that demands it.
- Missing indexes on frequently queried columns. Check slow query logs regularly.
- Ignoring replication lag. If you write to the primary and immediately read from a replica, you may get stale data. Use read-your-writes consistency where it matters.
- No backup strategy. Replication is not backup. A bad
DELETEstatement replicates to all replicas instantly. Maintain point-in-time recovery. - Over-normalizing. In high-read systems, denormalization (storing precomputed data) can eliminate expensive joins. Accept some data redundancy for performance.
Key Takeaways
- Relational databases are the safe default. Move to NoSQL when a specific workload demands it.
- ACID guarantees correctness; BASE trades consistency for availability and scalability.
- Indexes are the single biggest lever for read performance. Composite index column order matters.
- Replication provides read scalability and availability. Understand the consistency trade-offs of async replication.
- Shard only when you must, and choose a shard key that matches your primary access pattern.
- Polyglot persistence — using multiple databases for different workloads — is common and practical at scale.