System Design · Draft

31 — Reference tables

These tables collect the track’s main choices in one place. Each row is a summary, and the linked chapter has the reasoning, the caveats and the measurements behind it. Product behavior changes between versions, so the product-specific rows describe the current documentation, and are worth rechecking before a decision depends on them.

Data stores by access pattern

The first question is the access pattern, and the product comes second (chapter 6).

ModelFitsFits poorlyExamples
RelationalVaried and changing queries, joins, constraints, multi-row transactions, moneyWrite rates beyond one primary, unless partitioned or shardedPostgreSQL
Key-valueGet and put by key at high rates: sessions, carts, counters, cachesAny query that isn’t by keyRedis, DynamoDB
DocumentWhole aggregates read and written together, records whose shape variesJoins across documents, many-to-many relationshipsMongoDB, or PostgreSQL jsonb
Wide-columnVery high write rates, reads by a known partition key and a range inside itQueries not planned into the table design, ad hoc filteringCassandra
GraphMulti-hop traversals run constantly: networks, dependencies, recommendationsBulk scans and aggregatesNeo4j
SearchFull-text search, relevance ranking, facetsBeing the only copy of the data, strict read-after-writeElasticsearch
Time-seriesAppends by time, range scans, downsampling, retention by ageUpdates to old points, high-cardinality labelsPrometheus, or PostgreSQL partitioned by time
Object storageLarge immutable blobs: media, backups, logs, data lake filesSmall frequent updates, queries inside objects, multi-object atomicityAmazon S3 and compatible stores

Scaling and guarantees, product by product

ProductScales reads byScales writes byTransactionsRead consistency
PostgreSQLHot standby replicas that answer read-only queriesA bigger primary, batching; declarative partitioning splits a table on one server; extensions such as Citus shard across nodesAny set of statements; default isolation is Read Committed, with Repeatable Read and Serializable availablePrimary is current; the documentation calls standby data eventually consistent with the primary
RedisReplicasRedis Cluster, which spreads 16,384 hash slots across primariesMULTI/EXEC runs queued commands without other clients interleaving, with no rollback; scripts are also transactional; in a cluster, multi-key operations need keys in one hash slotReplication is asynchronous by default; WAIT reduces but doesn’t remove the chance of losing acknowledged writes on failover
DynamoDBPartitions chosen by a hash of the partition key, managed by the serviceSame, with partitions added automatically as throughput or storage growsTransactWriteItems: up to 100 actions, 4 MB in total, same account and RegionEventually consistent by default; strongly consistent reads optional on tables and local secondary indexes, never on global secondary indexes
MongoDBReplica set secondariesSharded clustersSingle-document operations are atomic; multi-document transactions need feature compatibility version 4.0 on a replica set and 4.2 on a sharded clusterSet per operation by read concern: local by default, with majority, linearizable and others available
CassandraAdding nodes; more replicasAdding nodesLightweight transactions: compare-and-set within a single partition; no cross-partition transactions, per its architecture documentationTunable per query: ONE, QUORUM, LOCAL_QUORUM, ALL
ElasticsearchReplica shardsPrimary shardsNone across documentsNear real-time: refreshed every second by default, on indices searched in the last 30 seconds
PrometheusA single server’s local storage; larger setups use external storage through the remote read and write APIsSameNoneLocal storage isn’t clustered or replicated, and the documentation says to manage it like any other single-node database
Amazon S3Managed by the serviceManaged by the serviceNone across keys; updates to a single key are atomicStrong read-after-write for PUT and DELETE; bucket configuration is eventually consistent

Notes on these rows:

  • PostgreSQL read replicas can return stale data, and long queries on a standby can be cancelled when they conflict with changes being replayed (chapter 12).
  • DynamoDB transactions cost two underlying reads or writes per item, one to prepare and one to commit, and conflicting transactions are cancelled. Transactions apply only within the Region where they’re called. In global tables, the default multi-Region mode is eventually consistent, and replicas in other Regions can see a transaction partly applied while it replicates.
  • MongoDB’s documentation says a distributed transaction usually costs more than single-document writes, and that for many cases an embedded data model remains the better choice.
  • Cassandra quorums overlap when the write and read replica counts satisfy W+R>RFW + R > RF, so QUORUM writes followed by QUORUM reads see the latest acknowledged write.
  • Redis Cluster is best-effort about write safety: its specification says there are usually small windows where acknowledged writes can be lost.
  • Redis durability depends on persistence settings. With appendfsync everysec, the default for the append-only file, a crash can lose about one second of writes. RDB snapshots alone can lose minutes.
  • S3 doesn’t lock objects for concurrent writers. If two PUT requests to one key overlap, the one with the latest timestamp wins.

Scaling patterns by bottleneck

A scaling change helps only if it targets the resource that’s actually running out (chapter 2, chapter 4).

BottleneckSymptomFixCostChapter
Application CPUHigh CPU on app servers, latency rising with loadStateless servers behind a load balancer, more instancesState has to move out of the servers5
Distance to usersHigh latency for far users, with fast server timesCDN, regional deploymentsCache invalidation, data placement1, 5
Connection setupMany new connections, handshake time dominatesKeep-alive, connection pools, HTTP/2 or HTTP/3Long-lived connections to manage1, 2
Database readsRead latency and database CPU rising togetherCache, read replicas, better indexesStaleness, invalidation, replica lag6, 7, 12
Database connectionsPool timeouts while the database itself is idleA connection pooler, shorter transactions, fewer slow queriesOne more component6
Database writesWrite latency, commit waits, I/O saturationBatching, group commit, asynchronous work, then partitioningLatency from batching; partitioning is hard to undo2, 8, 11
Data volumeStorage growth, slow maintenance, long backupsPartition by time, archive to object storage, retention, compressionQueries across tiers cost more11, 14
Hot keyOne cache node or shard far busier than the restLocal caching, replicating the key, splitting it into sub-keysStaleness, more complex reads7, 11
Slow work in the request pathRequests waiting on email, images, reportsA queue and background workersEventual completion, retries, duplicates8
Continuous event processingBatch jobs falling behind, stale aggregatesStream processing with windows and checkpointsState management, late data9
Load spikesBrief overload, then recoveryQueues as buffers, autoscaling, load sheddingDelay, or rejected requests10, 16
Fan-out tail latencyp99 of a page far above the p99 of each backendFewer calls in series, hedged requests, timeoutsExtra load from hedging10
CoordinationLock waits, contention on a single row or leaderPartition the contended state, avoid global locksWeaker ordering across partitions11, 13

Consistency at a glance

ModelGuaranteeTypical source
LinearizableEach operation appears to happen at one instant, in real-time orderA single leader read from the leader; consensus systems
SequentialAll nodes see one order, not necessarily real-timeReplicated state machines
CausalCausally related operations appear in orderSystems that track dependencies
Read-your-writesA client sees its own updatesReading from the leader after a write, or session tokens
Monotonic readsA client never sees older data after newerPinning a client to one replica
EventualReplicas converge if writes stopAsynchronous replicas, caches, search indexes

Chapter 12 covers each model. The defaults of common components:

ComponentDefault behaviorStronger option
PostgreSQL primaryRead Committed transactionsRepeatable Read or Serializable isolation
PostgreSQL hot standbyEventually consistent with the primarySynchronous standbys with synchronous_commit = remote_apply, or reading from the primary
DynamoDB table readEventually consistentConsistentRead on tables and local secondary indexes
DynamoDB global tableMulti-Region eventual consistencyMulti-Region strong consistency mode
CassandraSet per queryQUORUM reads and writes
Redis replicaAsynchronousWAIT for a number of acknowledgements, which still isn’t strong consistency
Amazon S3 objectStrong read-after-writeAlready strong for single keys
ElasticsearchVisible after the next refresh, about a secondAn explicit call to the refresh API
Cache in front of a databaseAs stale as the TTL or the invalidation path allowsDelete on write, leases, short TTLs (chapter 7)

Delivery guarantees at a glance

GuaranteeMeaningHow it’s achieved
At most onceMessages may be lost, but are never redeliveredSend once, never retry
At least onceMessages are never lost, but may be redeliveredRetry until acknowledged
Exactly onceThe effect happens onceAt-least-once delivery plus idempotent processing, or transactions within one system

Chapter 15 explains why exactly-once only holds within specific boundaries. What common systems provide:

SystemDeliveryOrderingNotes
Amazon SQS standard queueAt least onceBest effortConsumers must handle duplicates
Amazon SQS FIFO queueExactly-once processing within the queue, using deduplication IDsIn order within each message groupLower throughput than standard queues
Kafka producerIdempotent by default (enable.idempotence is true, acks is all)In order within a partitionTransactions across partitions since 0.11.0.0
Kafka consumerDepends on when offsets are committedIn order within a partitionDefault isolation is read_uncommitted, so aborted transactional messages are visible unless set to read_committed
Kafka to an external systemAt least once, unless the sink cooperatesPer partitionKafka’s documentation says exactly-once to other systems generally requires their cooperation
Stream processors with checkpointsExactly-once state, with transactional or idempotent sinksPer key or partitionChapter 9
HTTP API call with retriesAt least onceNoneIdempotency keys make retries safe (chapter 15)

With Kafka, acks=all waits for the current in-sync replicas, not every assigned replica. Setting min.insync.replicas makes the partition refuse writes when too few replicas are in sync, trading availability for durability.

Failure modes and mitigations

FailureWhat happensMitigationsChapter
A server crashesIts in-flight requests fail; in-memory state is lostSeveral stateless instances, health checks, retries of idempotent requests5, 16
A zone or datacenter is lostA share of capacity disappears at onceSpread across zones, headroom to absorb the loss3, 4
A dependency is slowThreads and connections fill up waiting; the slowness spreadsTimeouts, bulkheads, circuit breakers16
A retry stormRetries multiply load on a struggling serviceExponential backoff with jitter, retry budgets16
OverloadLatency rises sharply, then requests time outLoad shedding, backpressure, rate limiting10, 16, 22
A metastable failureThe system stays down after the trigger is goneShed load hard, drain queues, reduce retries16
A cache stampedeA hot key expires and many requests hit the database at onceRequest coalescing, leases, jittered TTLs, serving stale7
A cold cacheAfter a restart, all reads go to the databaseWarming, gradual traffic return, a database sized for misses4, 7
A hot key or shardOne node is overloaded while others are idleBetter partition keys, splitting the key, local caching11
Replica lagReads return old data; a user doesn’t see their own writeRead-your-writes routing, monitoring lag12
Failover with asynchronous replicationAcknowledged writes that hadn’t replicated are lostSynchronous or quorum replication for critical data12
Split brainTwo nodes both act as leaderConsensus, quorums, fencing tokens13
Clock skewTimestamps order events wrongly; leases misbehaveLogical clocks, bounded lease times, fencing13
A dual writeA database and a queue disagree after a partial failureTransactional outbox, change data capture15
A poison messageOne bad message blocks or crashes consumers repeatedlyRetry limits, dead-letter queues8
A queue backlogWork piles up faster than it’s processedMore consumers, backpressure, alerting on age of oldest message8
Disk full or flush misconfiguredWrites fail, or acknowledged data is lost on power failureCapacity alerts, retention, verified durable flushes2, 14
A bad deployment or configuration changeMany instances fail togetherCanary releases, gradual rollout, fast rollback20
An expired certificateTLS handshakes fail for every clientAutomated renewal, expiry alerts1, 19
DNS changes propagate slowlyClients keep using an old address until their cache expiresShort TTLs ahead of planned moves, failover below DNS1, 5
Connection pool exhaustionRequests wait for a connection while the database is idleShorter transactions, timeouts on acquiring a connection, fixing slow queries6

Numbers from chapter 4

The full table, with how each number was obtained, is in chapter 4. Measured numbers come from one laptop and differ on other hardware.

Latencies and rates

QuantityValueKind
Read from level-1 cacheAbout 1 nsMeasured
Random read from main memoryAbout 110 nsMeasured
Loopback TCP round trip16 µsMeasured
Random 4 KB SSD read, one at a time77 µs medianMeasured
Random 4 KB SSD reads, 64 in parallel130,000 a secondMeasured
Durable flush2.9 ms medianMeasured
Internet round tripAbout 9 ms near, 160 ms farMeasured
Trivial HTTP requests on one coreAbout 136,000 a secondMeasured
TCP setup1 round tripSourced (RFC 9293)
TLS 1.3 setup1 round tripSourced (RFC 8446)
TLS 1.2 setup2 round tripsMeasured, chapter 1
Light in a vacuumAbout 300 km per msSourced (SI definition)

Sizes and limits

QuantityValueKind
TCP initial window10 segments, at most 14,600 bytesSourced (RFC 6928)
PostgreSQL page8 kB by defaultSourced
PostgreSQL row overhead23-byte header plus 4-byte item identifierSourced
PostgreSQL default max_connectionsTypically 100Sourced
UUID16 bytesSourced (RFC 9562)
DynamoDB itemAt most 400 KBSourced
DynamoDB transactionUp to 100 actions, 4 MBSourced

Formulas

QuantityFormula
Average rateRequests a day ÷ 86,400
Peak factor from the busiest hour’s share hh24h24h
StorageRecords a day × bytes × days × (1 + index share) × copies
EgressPeak read rate × response size; × 8 for bits
Database reads behind a cacheRead rate × (1 − hit ratio)
ServersPeak ÷ (capacity per server × target utilization), rounded up
ConcurrencyL=λWL = \lambda W, then margin for peaks

Cache hit ratios under Zipf traffic

From the simulation in chapter 4, one million items, an LRU cache:

Share of items cacheds = 0.8s = 1.0s = 1.2
1%23.2%58.6%87.5%
10%48.7%77.9%95.0%
20%60.6%84.2%96.3%

The takeaway

Choose a store from the access pattern, and a scaling change from the resource that’s actually running out. Check the consistency and delivery guarantees of each component against what the data needs, because the defaults are usually weaker than the strongest option. Every failure mode in the table has a known mitigation, and every mitigation has a cost, which is why the chapters behind these tables matter more than the tables themselves.