Skip to content

08 — Database Selection

Choosing the right database is one of the most critical architectural decisions. The wrong choice leads to performance problems, complex workarounds, and painful migrations.

Analogy: Choosing a database is like choosing a vehicle. A sports car (Redis) is fast but can’t carry much cargo. A truck (PostgreSQL) is reliable and versatile. A cargo ship (S3) carries enormous loads but is slow. Use the right vehicle for each job.


Common database mistakes:

  • Using a relational DB for simple key-value data (overhead, schema rigidity)
  • Using NoSQL when you need ACID transactions and complex joins
  • Not planning for scale — picking a DB that can’t shard
  • Putting everything in one DB — no separation of concerns
  • Ignoring access patterns when choosing the data model

flowchart TB
App["Application"] --> DBChoice{"What are your<br/>data access patterns?"}
DBChoice -->|"Complex queries,<br/>relationships, ACID"| SQL["Relational DB<br/>PostgreSQL / MySQL"]
DBChoice -->|"Flexible schema,<br/>JSON documents"| Document["Document DB<br/>MongoDB"]
DBChoice -->|"Key-value lookups,<br/>high throughput"| KV["Key-Value Store<br/>Redis / DynamoDB"]
DBChoice -->|"Graph relationships,<br/>friend-of-friend"| Graph["Graph DB<br/>Neo4j"]
DBChoice -->|"Time series data,<br/>analytics"| TS["Time Series DB<br/>InfluxDB / TimescaleDB"]
DBChoice -->|"Full-text search"| Search["Search Engine<br/>Elasticsearch"]
style App fill:#f59e0b,color:#fff
style DBChoice fill:#7c3aed,color:#fff

FactorSQL (PostgreSQL, MySQL)NoSQL (MongoDB, DynamoDB)
SchemaFixed, rigidFlexible, dynamic
ACIDFull ACID supportBASE (eventual consistency)
JoinsNative, powerfulNot supported / app-level
ScalingVertical (primary), read replicasHorizontal (native sharding)
QueryComplex queries, aggregationsSimple key lookups, limited queries
Use CaseFinancial, enterprise, structuredRapid prototyping, large scale, flexible data

flowchart TB
CAP["CAP Theorem<br/>You can pick only 2 of 3"]
CP["Consistency + Partition Tolerance<br/>PostgreSQL, HBase<br/>Waits for consistency<br/>during partition"]
AP["Availability + Partition Tolerance<br/>DynamoDB, Cassandra<br/>Returns data even if<br/>it might be stale"]
CA["Consistency + Availability<br/>Single-site MySQL<br/>Can't handle network<br/>partitions"]
CAP --> CP & AP & CA
style CAP fill:#7c3aed,color:#fff
style CP fill:#3b82f6,color:#fff
style AP fill:#059669,color:#fff
style CA fill:#f59e0b,color:#fff

In real-world distributed systems, you choose between CP and AP — network partitions will happen, and you must choose consistency or availability when they do.


flowchart TB
Q1["Need ACID transactions<br/>and complex joins?"]
Q1 -->|"Yes"| Q2["High read/write throughput<br/>(>10K QPS)?"]
Q1 -->|"No"| Q3["Unstructured or<br/>flexible data?"]
Q2 -->|"Yes"| Shard["Sharded PostgreSQL<br/>or Vitess"]
Q2 -->|"No"| PG["PostgreSQL / MySQL<br/>Standard relational DB"]
Q3 -->|"Yes"| Q4["Simple key-value<br/>lookups?"]
Q3 -->|"No"| PG
Q4 -->|"Yes"| Q5["Need caching +<br/>real-time features?"]
Q4 -->|"No"| Mongo["MongoDB<br/>Document store"]
Q5 -->|"Yes"| Redis["Redis<br/>In-memory + persistence"]
Q5 -->|"No"| Dynamo["DynamoDB / Cassandra<br/>Key-value at scale"]

flowchart TB
subgraph Replication["Read Replicas"]
Primary["Primary DB<br/>Writes"]
Replica1["Read Replica 1"]
Replica2["Read Replica 2"]
Replica3["Read Replica 3"]
Primary --> Replica1 & Replica2 & Replica3
end
subgraph Sharding["Horizontal Sharding"]
Router["Query Router"]
Shard1["Shard 1<br/>users 0000-3333"]
Shard2["Shard 2<br/>users 3334-6666"]
Shard3["Shard 3<br/>users 6667-9999"]
Router --> Shard1 & Shard2 & Shard3
end
style Replication fill:#3b82f6,color:#fff
style Sharding fill:#7c3aed,color:#fff
StrategyDescriptionUse When
Read ReplicasCopy data to read-only instancesRead-heavy workloads (80/20 rule)
ShardingSplit data across databasesData too large for single DB
PartitioningSplit tables within same DBLarge tables, data lifecycle
FederationDifferent DBs for different domainsMicroservices, team ownership

PatternDescriptionExample
Single databaseOne DB for everythingSimple apps, monoliths
Database per serviceEach microservice owns its DBMicroservices architecture
CQRSSeparate read and write databasesHigh-throughput systems
Event sourcingStore events, derive stateAudit trails, financial systems
Polyglot persistenceUse different DBs for different needsBest tool for each job

ChoiceProsCons
Relational DBACID, strong consistencyHard to scale writes
NoSQLEasy to scale, flexibleNo joins, weaker consistency
Single DBSimple, ACIDDoesn’t scale, single point of failure
DB per serviceIndependent scalingData consistency across services
Polyglot persistenceOptimal for each use caseOperational complexity

StrategyWhenHow
Connection poolingAlwaysPgBouncer, HikariCP
Read replicasRead-heavy appsPostgreSQL streaming replication
Vertical scalingQuick fix, moderate scaleBigger instance (more CPU/RAM)
ShardingData > 1TB or writes > 10K QPSApplication-level or middleware
Caching layerRead-heavy, repeated queriesRedis in front of DB

  1. How do you choose between SQL and NoSQL for a new system?
  2. Explain the CAP theorem and how it affects database choice.
  3. How does sharding work and what are the challenges?
  4. What’s the difference between read replicas and sharding?
  5. How would you handle database migration from MySQL to DynamoDB?

SystemDatabase Strategy
AmazonDynamoDB (high-scale KV) + Aurora (relational) — polyglot
UberSchemaless (MySQL-based KV) → Docstore (custom)
TwitterManhattan (KV), MySQL (relational), FlockDB (graph)
NetflixCassandra (wide-column), EVCache (Redis), S3 (blobs)

  • SQL for complex queries, relationships, and ACID transactions
  • NoSQL for flexible schemas, massive scale, and simple access patterns
  • CAP theorem: In a distributed system, you choose between consistency and availability
  • Sharding splits data across databases — needed when one DB isn’t enough
  • Read replicas offload read traffic — great for read-heavy apps
  • Polyglot persistence = use multiple DB types — each for what it’s best at
  • The best database choice depends on your access patterns, not your data structure